Ejemplo de Device Code Flow de OAuth 2.0

¿Y si no tuviéramos un navegador en el dispositivo al que queremos dar permisos? Piensa en una televisión, una impresora, un asistente personal, un dispositivo IoT, etcétera. Por todo ello apareció un nuevo flujo en OAuth 2.0 llamado Device Code Flow. En este caso, el dispositivo en cuestión necesita que el usuario utilice otro dispositivo que tenga un navegador para iniciar sesión y otorgar el acceso a este primero.

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:

Copia el Application (client) ID y el Directory (tenant) ID, que serán necesarios para llamar al servidor de autorización. Estos puedes encontrarlos en el apartado Overview de la aplicación que acabas de registrar.

Azure AD – device-code-flow – application id y tenant id

Por último, en Authentication, debes habilitar a Yes el botón que se encuentra en el apartado Default Client Type, para que se trate al cliente como uno de tipo público (si no sabes lo que es esto, lee aquí), ya que no queremos guardar ningún secreto en nuestra aplicación.

Ya tienes todo lo que necesitas para poder ejecutar este flujo.

El cliente

Para este ejemplo, por simplicidad, no vamos a utilizar un dispositivo sin navegador. Vamos a simularlo con una aplicación web. Así que, como en los casos anteriores, he creado una aplicación web usando Node.js y Express:

//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: 'Device 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 DeviceCode_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/devicecode`;
    const Client_Id = process.env.CLIENT_ID;
    const Scope = 'https://graph.microsoft.com/User.Read';

    let body = `client_id=${Client_Id}&scope=${Scope}`;

    log.info(DeviceCode_Endpoint);

    fetch(DeviceCode_Endpoint, {
        method: 'POST',
        body: body,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    }).then(async response => {
        let json = await response.json();
        log.info(json);
        res.render('device-code', { code: JSON.stringify(json, undefined, 2), message: json.message, interval: json.interval, device_code: json.device_code }); //you shouldn't share the access token with the client-side

    }).catch(error => {
        log.error(error.message);
    });

});

//Step 2: Check if the user has signed and introduce the code
app.post('/checking', (req, res) => {

    const Token_Endpoint = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`;
    const Grant_Type = 'urn:ietf:params:oauth:grant-type:device_code';
    const Client_Id = process.env.CLIENT_ID;
    const Device_Code = req.body.device_code;

    let body = `grant_type=${Grant_Type}&client_id=${Client_Id}&device_code=${Device_Code}`;

    fetch(Token_Endpoint, {
        method: 'POST',
        body: body,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    }).then(async response => {

        let json = await response.json();
        log.info(json);

        if (response.ok) {
            res.send(200, json);
        }
        else {
            res.send(400);
        }

    }).catch(response => {
        log.error(response.error);
    });
});

//Step 3: Show the access token
app.get('/access/token', (req, res) => {

    res.render('access-token', { token: req.query.access_token }); //you shouldn't share the access token with the client-side

});

//Step 4: Call the protected API
app.post('/call/ms/graph', (req, res) => {

    let access_token = req.body.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);

Como ves, el código está dividido en pasos. Veamos uno a uno qué es lo que hacen:

Paso 1: Recuperar el código del dispositivo

Como primer paso, necesitamos que el servidor de autorización nos de un código asociado al dispositivo, en este caso nuestra web. En la página de inicio tenemos un botón que solicita el mismo.

Este hará una llamada a /get/the/code, que hará una llamada POST al endpoint /devicecode con el client id de la aplicación registrada y el scope que la aplicación necesita. El resultado será parecido al siguiente:

OAuth 2 – Device Code Flow – Wait for the user to validate the code

Como ves, el servidor de autorización ha devuelto un conjunto de valores:

  • user_code: es el código que el usuario debe utilizar para autorizar que el dispositivo actúe en su nombre.
  • device_code: lo utilizará nuestra aplicación como método de comprobación contra el endpoint /token, para saber si el usuario ha validado el código o qué ha hecho con él.
  • verification_uri: es la URL donde el usuario debe dirigirse para validar el código.
  • expires_in: es el tiempo en segundos en el que expirará el código.
  • interval: se trata del número de segundos que el cliente debería esperar entre comprobaciones.
  • message: se trata de un mensaje pensado para mostrarlo al usuario final. Simplemente recoge los valores verification_uri y user_code y los concatena en una frase.

En esta página, la aplicación está esperando a que el usuario acceda desde otro dispositivo a la dirección https://microsoft.com/devicelogin e introduzca el código de usuario, en este caso CBBE8LDVC.
¿Cómo sabe la aplicación si el usuario ha validado o no el código? Sencillo: este llamará al endpoint /token con el device_code cada 5 segundos, que es el valor de interval. Cuando el usuario valide el código satisfactoriamente se recibirá un 200. Mientras tanto, recibirá un 400 (Bad request). Para poder simularlo en este ejemplo, he creado un javascript en el cliente, que llama a /checking cada 5 segundos.

<script>

    (function () {

        let checking = setInterval(function () {
            fetch('/checking', {
                method: 'POST',
                body: `device_code=<%= device_code %>`,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            }).then(async response => {
                if (response.ok) {
                    //Stop checking
                    clearInterval(checking);
                    let json = await response.json();
                    let access_token = json.access_token;

                    //redirecting with the access token
                    window.location.replace(`http://localhost:8000/access/token/?access_token=${access_token}`);
                }
                else {
                    console.log('We have to wait');
                }
            })
        }, <%= interval %> * 1000);

    })();

</script>

De hecho, si abres las herramientas del desarrollador del navegador, verás que se hace una petición cada 5 segundos.

Cuando el usuario introduce el código, inicia sesión y acepta el consentimiento, nuestra aplicación web nos redirigirá a /access/token para mostrarnos el token (esto no sólo con fines educativos).

OAuth 2.0 – Device Code Flow – Get the access token

Si pulsas sobre el botón Call Microsoft Graph API, podrás hacer la llamada al recurso protegido con el token obtenido a través del flujo Device Code.

OAuth 2.0 – Device Code Flow – API response

Para que este ejemplo funcione, debes crear un archivo .env con los siguientes valores:

TENANT_ID="<YOUR TENANT ID>"
CLIENT_ID="<YOUR CLIENT ID>"

El código lo tienes en mi GitHub.

¡Saludos!