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!