Ejemplo de Authorization Code Flow de OAuth 2.0

Actualizado el 21/12/2022

Después de la explicación, viene el ejemplo 🤓 Ahora que ya sabes todos los flujos que existen a día de hoy, quiero ver contigo cada uno de ellos a través de un ejemplo.
No he querido utilizar ninguna librería para estos artículos. Como el objetivo es saber cómo funcionan, he querido evitar todo aquello que me oculte lo que está pasando realmente. La idea es basarme en los conceptos que te conté en el artículo anterior. Hoy vamos a ver el flujo Authorization Code y, para ello, vamos a trabajar con los siguientes componentes:

  • Recurso protegido (Protected Resource): Microsoft Graph API
  • Cliente (Client): aplicación en Node.js con Express de back end.
  • Usuario (Resource Owner): yo misma 👋
  • Servidor de autorización: Azure Active Directory

Configuración del servidor de autorización

Antes de que nuestro cliente sea capaz de pedir un token, para llamar al recurso protegido, primero es necesario registrar nuestro cliente en el servidor de autorización, en este caso Azure Active Directory.

Entra en el portal de Microsoft Azure, accede a Azure Active Directory y haz clic en la sección App registrations.

App registrations

Ahora haz clic en el botón New registration del menú, para registrar nuestra futura aplicación cliente. Los valores que debes introducir son los siguientes:

Registrar el cliente en Azure Active Directory
  • Name: se trata de un nombre identificativo del cliente dentro de Azure Active Directory.
  • Supported account types: Azure Active Directory te permite que los usuarios que se autentiquen, utilizando esta aplicación, sean del directorio activo en el que nos encontramos, de cualquier directorio activo de Azure AD o bien utilizando Microsoft Accounts. Para este ejemplo dejaremos la opción por defecto, que es la primera.
  • Redirect URI: tal y como te comenté en el artículo anterior, es necesario registrar una URL de nuestro sitio, para ser redirigidos cuando la autenticación y el consentimiento del usuario hayan concluido de manera satisfactoria. En este caso mi redirect uri será http://localhost:8000/give/me/the/code

Haz clic en Register en la parte inferior de la pantalla. Con esto, ya tenemos la aplicación registrada en el servidor de autorización. Sin embargo, antes de marcharte necesitamos recuperar algunos valores de aquí:

En primer lugar necesitamos recuperar el Client Id que nos ha asignado este servidor de autorización, al registrar nuestra aplicación. Cuando hiciste clic en el botón Register, este nos llevo al apartado Overview del nuevo registro. En él puedes encontrar el Client Id llamado Application (client) ID.

Otra de las cosas que el cliente necesitaba conocer eran los endpoints para la autorización y para la recuperación del token. Estos puedes encontrarlos haciendo clic en Endpoints.

Azure Active Directory – Endpoints

Para OAuth 2.0 necesitamos los dos primeros. Si te fijas, tal y como vimos en el apartado endpoints del artículo anterior, uno de ellos es el de autorización (/authorize) y el otro el que nos dará el token (/token).

Lo siguiente que necesitamos es identificar qué scope queremos para Microsoft Graph, o lo que es lo mismo: qué acciones queremos ejecutar de esta. Estos son propios de cada API, por lo que necesitamos conocer los que ofrece cada una de antemano. Los scopes a los que tiene acceso una aplicación se encuentran en la sección API Permissions.

Permisos para usar Microsoft Graph

Cuando registras tu aplicación en Azure Active Directory se te asigna uno por defecto, que es User.Read de la API de Microsoft Graph. Para este ejemplo es más que suficiente. Si quisieras añadir más puedes hacerlo a través del botón Add a permission.

Para finalizar, necesitamos generar un secreto, ya que en el flujo Authorization Code es necesario a la hora de intercambiar el código de autorización por un token de acceso. En la sección Certificates & secrets haz clic en New client secret. Elige un año como tiempo de expiración del mismo y haz clic en Add.

Azure AD – Generar un client secret para nuestra aplicación

Ahora ya tenemos todo lo necesario para crear nuestro cliente y llevar a cabo este flujo.

El cliente

Como te decía, he desarrollado la aplicación en Node.js, sin ninguna librería que me ayude con el flujo de OAuth 2.0. He conseguido que todo ocurra en 100 líneas y que sea lo más legible posible 🙂

//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: 'Authorization Code 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');
});
//Set 1: Ask the authorization code
app.get('/get/the/code', (req, res) => {
    const Authorization_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/authorize`;
    const Response_Type = 'code';
    const Client_Id = process.env.CLIENT_ID;
    const Redirect_Uri = 'http://localhost:8000/give/me/the/code';
    const Scope = process.env.SCOPE;
    const State = 'ThisIsMyStateValue';
    let url = `${Authorization_Endpoint}?response_type=${Response_Type}&client_id=${Client_Id}&redirect_uri=${Redirect_Uri}&scope=${Scope}&state=${State}`;
    log.info(url);
    res.redirect(url);
});
//Step 2: Get the code from the URL
app.get('/give/me/the/code', (req, res) => {
    //before continue, you should check that req.query.state is the same that the state you sent
    res.render('exchange-code', { code: req.query.code, state: req.query.state });
});
//Step 3: Exchange the code for a token
app.post('/exchange/the/code/for/a/token', (req, res) => {
    const Token_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`;
    const Grant_Type = 'authorization_code';
    const Code = req.body.code;
    const Redirect_Uri = 'http://localhost:8000/give/me/the/code';
    const Client_Id = process.env.CLIENT_ID;
    const Client_Secret = process.env.CLIENT_SECRET;
    const Scope = process.env.SCOPE;
    let body = `grant_type=${Grant_Type}&code=${Code}&redirect_uri=${encodeURIComponent(Redirect_Uri)}&client_id=${Client_Id}&client_secret=${Client_Secret}&scope=${encodeURIComponent(Scope)}`;
    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 4: 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);

Cada uno de los pasos del flujo lo he ido acompañando de un paso a paso en el lado del cliente, para que puedas ir viendo el resultado. Nunca debes compartir con el lado del cliente el access token, ya que entonces pierde toda la gracia 🙂 En este ejemplo lo hago solo por motivos educativos.

Puedes descargar el ejemplo completo desde mi GitHub y lanzar npm start para ejecutarlo en http://localhost:8000.

Para que este ejemplo funcione crea un archivo .env, o renombra este archivo, y añade las siguientes variables con sus correspondientes valores:

TENANT_ID="<Directory (tenant) ID>"
CLIENT_ID="<Application (client) ID>"
CLIENT_SECRET="<Client secret>"
SCOPE="https://graph.microsoft.com/User.Read"

Explicación del código fuente

Paso 1: Conseguir el código de autorización

Para obtener el código debemos redirigir al usuario al servidor de autorización, donde tendrá que iniciar sesión y aceptar el consentimiento. Normalmente esta acción se suele hacer con un enlace o un botón, como se ve en la imagen anterior. Al hacer clic en él se ejecuta el paso 1 del servidor:

//Set 1: Ask the authorization code
app.get('/get/the/code', (req, res) => {
    const Authorization_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/authorize`;
    const Response_Type = 'code';
    const Client_Id = process.env.CLIENT_ID;
    const Redirect_Uri = 'http://localhost:8000/give/me/the/code';
    const Scope = process.env.SCOPE;
    const State = 'ThisIsMyStateValue';
    let url = `${Authorization_Endpoint}?response_type=${Response_Type}&client_id=${Client_Id}&redirect_uri=${Redirect_Uri}&scope=${Scope}&state=${State}`;
    log.info(url);
    res.redirect(url);
});

Lo que hacemos en él es recuperar todos los valores que debemos adjuntar a la redirección que vamos a hacer al servidor de autenticación, en este caso Azure Active Directory. Concatenamos todo y utilizamos res.redirect para redirigir al usuario. Una vez allí, deberás iniciar sesión con un usuario de tu directorio y aceptar los permisos que solicita el cliente.

Azure Active Directory – Consent

Cuando el proceso de autenticación finalice, serás redirigido de nuevo a la aplicación utilizando la URL http://localhost:8000/give/me/the/code

Paso 2: Recuperar el código de la URL

Cuando eres redirigido a http://localhost:8000/give/me/the/code, por parte del servidor de autorización, este añade como query string el código de autorización y el state. Es nuestra labor recuperar ambos valores, como se ve en el paso 2:

//Step 2: Get the code from the URL
app.get('/give/me/the/code', (req, res) => {
    //before continue, you should check that req.query.state is the same that the state you sent
    res.render('exchange-code', { code: req.query.code, state: req.query.state });
});

Lo normal es que el back end cambie este código por un token nada más recibirlo, además de comprobar que el valor de state es el mismo que él envió. Sin embargo, en este ejemplo lo estoy enviando a una vista llamada exchange-code para ver el contenido en la web, siempre con fines educativos:

Paso 3: Cambiar el código por el token de acceso

Cuando haces clic en el botón Exchange the code for an access token lo que ocurre es justo eso:

//Step 3: Exchange the code for a token
app.post('/exchange/the/code/for/a/token', (req, res) => {
    const Token_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`;
    const Grant_Type = 'authorization_code';
    const Code = req.body.code;
    const Redirect_Uri = 'http://localhost:8000/give/me/the/code';
    const Client_Id = process.env.CLIENT_ID;
    const Client_Secret = process.env.CLIENT_SECRET;
    const Scope = process.env.SCOPE;
    let body = `grant_type=${Grant_Type}&code=${Code}&redirect_uri=${encodeURIComponent(Redirect_Uri)}&client_id=${Client_Id}&client_secret=${Client_Secret}&scope=${encodeURIComponent(Scope)}`;
    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);
    });
});

Creamos el cuerpo de una llamada POST al endpoint /token con todos los valores que indica la especificación: el grant_type es authorization_code, el código de autorización que hemos recuperado, de nuevo la redirect_uri, el client id, el client secret (que en este flujo es obligatorio) y el scope. Si todo está correcto, el servidor nos devolverá un token como este:

Paso 4: Llamar al recurso protegido con el token de acceso

Ya tenemos todo lo que necesitamos para poder llamar al recurso protegido, en este caso a la API de Microsoft Graph. Gracias a OAuth 2.0 esta API tiene bien delimitado qué se puede hacer y qué no gracias a los scopes, por lo que no podemos llamar a cualquier acción de Microsoft Graph. Con el scope User.Read sólo podemos leer información de nosotros mismos, y eso es lo que hacemos en el paso 4 del código.

//Step 4: 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) });
    });
});

Si haces clic en el botón Call Microsoft Graph API podrás ver el resultado de esta llamada.

El código completo lo tienes en mi GitHub.

¡Saludos!

4 Comments

  1. Michael Bolanos septiembre 27, 2019 at 1:39 pm

    Genial articulo; no entiendo como la documentacion oficial de Microsoft es tan dificil de seguir. Gracias a tus articulos me he podido poner al hilo.

    Excelente forma de redactar.

    Reply
    1. Gisela Torres septiembre 28, 2019 at 1:09 am

      ¡Gracias Michael! Me alegro de que te haya sido de utilidad.

      Reply
  2. Fekri Essady noviembre 29, 2019 at 10:48 am

    Buenos días,

    En primer lugar darte las gracias por dedicar tu tiempo a documentar este post y los anteriores. En segundo lugar tengo una duda, Cuando intento visualizar los endpoint en azure ad, no encuentro la opción, supongo que es necesario obtener una licencia para hacer esto funcionar.

    Espero tu respuesta.
    Gracias,

    Reply
  3. Ivan Mtz mayo 11, 2020 at 4:19 pm

    Hola Gisela, muchas gracias por tu explicación.
    Crees que haya esto mismo para Java?

    Reply

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.