Ejemplo de Client Credentials Flow de OAuth 2.0

Hay aplicaciones donde no necesitamos interactuar en nombre de los usuarios. Más bien, si se realiza cualquier tipo de acción, se hará en nombre de la propia aplicación. En el artículo de hoy vamos a ver el flujo Client Credentials, que está pensado justo para este escenario. Al igual que en el flujo Authorization Code e Implicit Flow, vamos a llamar a la API de Microsoft Graph, pero desde una perspectiva diferente.

Configuración del servidor de autorización

Como en los flujos anteriores, necesitamos registrar nuestra aplicación 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 (Preview).

Azure Active Directory – App registrations – preview

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:

Azure AD – Register an application – client-credentias-flow-example

En este caso sólo añadiremos el nombre de la aplicación que vamos a registrar. No es necesario asignar una redirect URI, ya que llamaremos directamente al endpoint de obtención del token.

Recupera el Application (client) ID y el Directory (tenant) ID para después, en el apartado Overview.

Como te decía, en este flujo sólo nos hará falta el Token Endpoint, el cual puedes recuperar en el apartado Endpoints, justo encima de los valores que acabas de copiar:

Azure AD – Endpoints – Token endpoint

Lo siguiente que necesitas es generar un secreto para poder realizar la llamada al token endpoint. En el apartado Certificates & secrets haz clic en New client secret.

Azure AD – Certificates & secrets – New client secret

Por último, necesitamos modificar el scope por defecto de Microsoft Graph, ya que el que se asigna de manera automática cuando registras una aplicación es para acceder a la información del usuario que se ha autenticado. En este flujo la aplicación cliente nunca representará a un usuario, por lo que vamos a modificarlo para que lea la información de todos los usuarios 🙂 El scope que necesitamos es User.Read.All. En el apartado API Permissions haz clic en Add a permission, selecciona la API Microsoft Graph y, en la parte superior, haz clic sobre Application Permissions, ya que no hay usuarios involucrados.

API Permissions – Add a permission – MS Graph – Application Permissions

Dentro del listado, abajo del todo, selecciona dentro de User el llamado User.Read.All.

Microsoft Graph Scopes – User.Read.All

Si recuerdas el primer artículo de esta serie, cuando queremos acceder a una API siempre necesitamos el consentimiento del usuario antes de devolver el token. En este caso, no hay ningún usuario que se autentique, por lo que de alguna forma debemos confirmar que esta aplicación tiene el consentimiento, aunque sea a través de un administrador del servidor de autorización, de hacer lo que dice que quiere hacer. En este caso, en el mismo apartado de API permissions tenemos un botón en la parte inferior que hace justamente esto. Haz clic en Grant admin consent for returngis para aprobar que el cliente pueda solicitar acceso a los permisos que necesita para funcionar.

Con esto ya tendrías el servidor de autenticación configurado para la aplicación cliente.

El cliente

Lo primero que me he creado es un archivo con las variables de entorno (.env) con los siguiente valores:

TENANT_ID="<DIRECTORY (TENANT) ID>"
CLIENT_ID="<APPLICATION (CLIENT) ID>"
CLIENT_SECRET="<CLIENT SECRET>"

En este caso, al ser un cliente del tipo confidencial (es decir, del que puede guardar secretos), tenemos una parte en el backend hecha con Node.js y Express en un archivo llamado server.js:

//Modules
const express = require('express'),
    bunyan = require('bunyan'),
    bodyParser = require('body-parser'),
    fetch = require("node-fetch");

//Load values from .env file
require('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');
});

//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 = 'client_credentials';    
    const Client_Id = process.env.CLIENT_ID;
    const Client_Secret = process.env.CLIENT_SECRET;
    const Scope = 'https://graph.microsoft.com/.default';

    let body = `grant_type=${Grant_Type}&client_id=${Client_Id}&client_secret=${encodeURIComponent(Client_Secret)}&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 = '/users';

    //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);

Como puedes ver a través del código, tenemos únicamente dos pasos:

Paso 1: Recuperar el access token

En la página Index tengo un botón que llama a la URL /get/the/token.

OAuth 2.0 Client Credentials Flow – Conseguir un access token

Para recuperarlo, se monta la petición a realizar al endpoint /token con el grant_type del tipo client_credentials, el client_id, el client_secret y el scope que, en este caso será https://graph.microsoft.com/.default. Una vez que hemos recuperado el token, a modo educativo, devolvemos el mismo al cliente (esto no deberías hacerlo en un caso real).

OAuth 2.0 Client Credentials Flow – Access token recuperado

Ahora que ya tenemos un access token válido, podemos llamar a la API de Microsoft Graph, en este caso para recuperar información de todos los usuarios de mi directorio. En el segundo paso, cuando hacemos un post a /call/ms/graph recuperamos el token que nos viene esta vez del lado del cliente y con él hacemos la llamada a la API. El resultado será parecido al siguiente:

OAuth 2.0 Client Credentials Flow – Resultado de la llamada a Microsoft Graph

El código lo tienes en mi GitHub.

¡Saludos!