Ejemplo de Resource Owner Password Credentials (ROPC) Flow de OAuth 2.0

Actualizado el 23/12/2022

Ya hemos visto cómo usar OAuth 2.0 a través de los flujos Authorization Code, Implicit y Client Credentials. Todos ellos están enfocados a que las aplicaciones modernas tengan la mínima exposición de las credenciales de sus usuarios, así como afinar los permisos o el poder que tiene una aplicación sobre los recursos a los que intenta acceder. Sin embargo, esto no es siempre la realidad que nos encontramos. Si estás pensando en cómo integrar OAuth 2.0 en aplicaciones legacy , como por ejemplo, aplicaciones que todavía usan HTTP Basic Authentication, el flujo Resource Owner Password Credentials es de tu interés. Esta debería de ser una solución temporal, para dar tiempo a cambiar el tipo de autenticación de la aplicación, ya que en este caso mandamos directamente el nombre de usuario y la contraseña para obtener el token. En este artículo veremos el mismo ejemplo que en los anteriores con la API de Microsoft Graph.

Configuración del servidor de autorización

Dentro del portal de Azure, accede a tu Azure Active Directory y haz clic en la sección App Registrations (Preview).

Azure Active Directory – App registrations – preview

Dentro de esta sección vamos a registrar nuestra nueva aplicación cliente, llamada resource-owner-password-credentials-flow-example.

Registrar la aplicación en Azure AD

En este caso, como no vamos a llamar al endpoint de autorización no necesitamos especificar ninguna URL de retorno.
Como en los flujos anteriores, copia el Application (client) ID y el Directory (tenant) ID.

copiar client id y tenant id

Además, es necesario generar un secreto para poder recuperar el token de acceso a través del endpoint token. Accede al apartado Certificates & secrets y haz clic en el botón New client secret. Elige una descripción y haz clic en Add.

Generar un secreto

Como ocurría con el flujo Client Credentials, un administrador de Azure Active Directory debe consentir que la aplicación haga acciones en nombre de los usuarios de su directorio, ya que ellos no van a poder aceptar, o no, el consentimiento, como ocurría en los flujos Authorization Code e Implicit Flow.

Ahora ya tienes todo lo que necesitas para crear tu cliente.

El cliente

En este caso toda la interacción con el servidor de autorización se va a hacer en el back end. Recuerda que cada vez que trabajamos con un flujo que necesita de un secreto nuestra aplicación debe tener un back end donde poder guardarlo de manera segura.

Copia el archivo .env.sample (llámalo .env) y añade los valores anteriores:

TENANT_ID="<YOUR_TENANT_ID>"
CLIENT_ID="<YOUR_CLIENT_ID>"
CLIENT_SECRET="<YOUR_CLIENT_SECRET>"
USER_NAME="<YOUR_USER_NAME>"
USER_PASSWORD="<YOUR_PASSWORD>"

Como en el resto de los artículos, vamos a utilizar Node.js y Express:

//Modules
import express from 'express';
import bunyan from 'bunyan';
import bodyParser from 'body-parser';
import fetch from 'node-fetch';
//Load values from .env file
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const log = bunyan.createLogger({ name: 'Resource Owner Password Credentials Flow' });
app.use(express.static('public'));
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
    res.render('index');
});
//Step 1: Get the access token
app.get('/get/the/token', (req, res) => {
    const Token_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`;
    const Grant_Type = 'password';    
    const Client_Id = process.env.CLIENT_ID;
    const Client_Secret = process.env.CLIENT_SECRET;
    const UserName = process.env.USER_NAME;
    const Password = process.env.USER_PASSWORD;
    const Scope = 'https://graph.microsoft.com/User.Read';
    let body = `grant_type=${Grant_Type}&client_id=${Client_Id}&client_secret=${Client_Secret}&username=${UserName}&password=${Password}&scope=${encodeURIComponent(Scope)}`;
    log.info(`Endpoint: ${Token_Endpoint}`);
    log.info(`Body: ${body}`);
    fetch(Token_Endpoint, {
        method: 'POST',
        body: body,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'           
        }
    }).then(async response => {
        let json = await response.json();
        res.render('access-token', { token: JSON.stringify(json, undefined, 2) }); //you shouldn't share the access token with the client-side
    }).catch(error => {
        log.error(error.message);
    });
});
//Step 2: Call the protected API
app.post('/call/ms/graph', (req, res) => {
    let access_token = JSON.parse(req.body.token).access_token;
    const Microsoft_Graph_Endpoint = 'https://graph.microsoft.com/beta';
    const Acction_That_I_Have_Access_Because_Of_My_Scope = '/me';
    //Call Microsoft Graph with your access token
    fetch(`${Microsoft_Graph_Endpoint}${Acction_That_I_Have_Access_Because_Of_My_Scope}`, {
        headers: {
            'Authorization': `Bearer ${access_token}`
        }
    }).then(async response => {
        let json = await response.json();
        res.render('calling-ms-graph', { response: JSON.stringify(json, undefined, 2) });
    });
});
app.listen(8000);

Ahora vamos a seguir los pasos que vemos en el código.

Paso 1: obtener el accesss token

Como nuestro objetivo es poder llamar a la API de Microsoft Graph, lo primero que necesitamos es recuperar el token de acceso. Para ello tenemos un botón en la página de inicio que hará la llamada al servidor de autorización.

En este flujo, llamaremos al endpoint /token sin pasar por el de autorización. Pasaremos como grant type el valor password y le mandaremos el client id, client secret, el nombre del usuario así como su contraseña. Si todos los ratos son correcto, recibiremos el access token directamente.

Paso 2: Llamar al recurso protegido con el access token recibido

Con el token en mano ya podemos llamar al recurso protegido, en este caso la API de Microsoft Graph. Simplemente pulsa el botón que aparece justo debajo del token de acceso y se realizará la llamada con el mismo en la cabecera de la petición.

Como te decía, este flujo solo debe de ser considerado en aplicaciones legacy que pretenden ser migradas en breve a otro de los flujos mencionados.

El código lo tienes en mi GitHub.

¡Saludos!