Hace algo más de tres años escribí una serie de artículos sobre OAuth 2.0 y los diferentes flujos que podíamos utilizar en nuestras aplicaciones. Lo cierto es que es una de las series más vistas a diario en mi blog y es por ello que estas navidades he estado actualizando los artículos más relevantes de la misma, así como el código que ya estaba obsoleto, en cuanto a librerías y alguna cosita más. En todo este tiempo, algunas cosas han cambiado: En el momento que escribí los artículos el flujo recomendado para aplicaciones del tipo Single Page Application era Implicit Flow, pero desde hace bastante tiempo la recomendación es la misma que para las aplicaciones nativas, esto es el flujo Authorization Code + PKCE. En este artículo quiero mostrarte cómo funciona con este tipo de aplicaciones.
Configuración del servidor de autorización
Al igual que en el resto de la serie, voy a utilizar Azure Active Directory como servidor de autorización. Para dar de alta la aplicación de ejemplo que te voy a mostrar, debes ir antes al portal de Microsoft Azure, acceder al recurso Azure Active Directory y haz clic en la sección App registrations.
En ella utiliza el botón New registration del menú, para darla de alta. Elige un nombre para la misma, en mi caso la he llamado oauth2-spa-auth-code-flow-pkce, y selecciona en el desplegable del apartado Redirect URI (optional) la opción Single-page application (SPA) y la URL «de vuelta», una vez que el usuario se haya autenticado.
Haz clic en el botón Register y guarda los valores Application (client) ID y Directory (tenant) ID del apartado Overview.

Para comprobar que la has registrado correctamente, si vas al apartado Authentication debería aparecerte un mensaje, en el apartado Single-page Application de Platform configurations que dice «Your Redirect URI is eligible for the Authorization Code Flow with PKCE.»
Del lado del servidor de autorización ya tendrías todo listo para este cliente.
El cliente
Todo el código del ejemplo explicado aquí lo tienes en este repo de GitHub. Este no está pensado para un uso productivo, sino con fines educativos, donde el principal objetivo es entender cómo funciona el flujo y cuáles son los pasos que da. En el caso de Azure AD, y Azure AD B2C, la recomendación es usar MSAL.js, que hace todo lo que te explico aquí de manera transparente para ti.
En mi código de ejemplo he querido que la misma ruta sepa gestionar todo lo que debe ir haciendo, como si de una SPA se tratase. Esta tiene la siguiente pinta:
Como ves, la misma la he dividido en 4 pasos diferentes. Déjame contarte cada uno de ellos, para que entiendas lo que hacen.
Paso 1: Generar code verifier y code challenge
Si has revisado de qué trata PKCE (o Proof Key for Code Exchange), lo que hacemos es enviar un código adicional en cada una de las peticiones al servidor de autorización, de tal forma que la primera y la segunda llamada se pueda comprobar que son del mismo cliente y no porque alguien nos haya robado el código de intercambio para el access token. En mi ejemplo tienes estas dos funciones:
/* 1. Generate verifier */
function generateRandomString(length) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
/* 2. Hashing the verifier */
async function generateCodeChallenge(codeVerifier) {
var digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
La primera de ellas genera el code verifier, que no es más que una cadena con caracteres aleatorios entre 43 y 128 de tamaño. El code challenge es ese code verifier hasheado, usando en este caso SHA-256, aunque también puede ir en plano. Cuando se carga nuestra página, a nivel de JavaScript, ocurre lo siguiente:
(async () => {
const TENANT_ID = '<TENANT_ID>';
const CLIENT_ID = '<CLIENT_ID>';
const SCOPE = 'https://graph.microsoft.com/User.Read';
// Try to get the code from the query string
const urlParams = new URLSearchParams(window.location.search);
let code = urlParams.get('code');
if (!code) {
activeStep('1');
let state = generateRandomString(12);
let code_verifier = generateRandomString(128);
/* Storing the verifier */
window.sessionStorage.setItem("code_verifier", code_verifier);
let code_challenge = await generateCodeChallenge(code_verifier);
id('code_verifier').innerText = code_verifier;
id('code_challenge').innerText = code_challenge;
const url = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/authorize?client_id=${CLIENT_ID}
&response_type=code
&state=${state}
&code_challenge=${code_challenge}
&code_challenge_method=S256
&redirect_uri=http://localhost:8000
&scope=${SCOPE}`;
document.getElementById('login').setAttribute('href', url);
En primer lugar, debo incluir los valores copiados anteriormente, cuando registré mi aplicación en Azure AD, para TENANT_ID y CLIENT_ID. Compruebo si como parte de la URL hay un parámetro llamado code, que veremos en el paso 2. Como la primera vez no va a existir genero el valor de state (opcional), el code_verifier, el cual almaceno en la sesión, y el code challenge, pasando como parámetro el code verifier generado. Muestro ambos por pantalla, como decía, con fines educativos. El objetivo de todo esto es que con este código, el tenant id, client id, state y el scope generamos la URL para dirigir al usuario al servidor de autorización, en este caso Azure AD, para que se autentique. Cuando el proceso es satisfactorio vamos al paso 2.
Pasos 2 y 3: Intercambiar el código (y code verifier) por el access token
Cuando finaliza la autenticación Azure AD redirecciona al usuario de nuevo al cliente, pero esta vez con un parámetro llamado code como parte de la URL a la que se redirige.

Cuando esto ocurre, el if anterior no se cumple y pasamos al else o, lo que es lo mismo, el paso dos:
else {
activeStep('2');
id('code').innerText = code;
id('btnExchangeCodeForAccessToken').addEventListener('click', () => {
let body = `grant_type=authorization_code&code=${code}
&redirect_uri=${encodeURIComponent('http://localhost:8000')}
&client_id=${CLIENT_ID}
&code_verifier=${window.sessionStorage.getItem('code_verifier')}
&scope=${encodeURIComponent(SCOPE)}`;
fetch(`https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`,
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body
}
).then(response => response.json())
.then((response) => {
activeStep('3');
window.sessionStorage.setItem('access_token', response.access_token);
id('access_token').innerHTML = JSON.stringify(response, undefined, 2);;
Además de mostrar el código en la interfaz también habilito el botón con id btnExchangeCodeForAccessToken para intercambiar este código por un access token. Para que el intercambio sea satisfactorio hay que adjuntar a la petición el code verifier que generamos al principio, para que el servidor de autorización valide que «somos los mismos» a los que le dio el código en primera instancia. Si todo ha ido bien la petición nos devolverá el access token, que sería el paso tres, con el que poder hacer llamadas.

Paso 4: Llamar a Microsoft Graph
Para comprobar que el token recibido es válido, lo único que queda es llamar a Microsoft Graph:
id('btnCallAPI').addEventListener('click', () => {
activeStep('4');
var result = document.getElementById('result');
const Microsoft_Graph_Endpoint = 'https://graph.microsoft.com/beta';
const Acction_That_I_Have_Access_Because_Of_My_Scope = '/me';
fetch(`${Microsoft_Graph_Endpoint}${Acction_That_I_Have_Access_Because_Of_My_Scope}`, {
headers: {
'Authorization': `Bearer ${window.sessionStorage.getItem('access_token')}`
}
}).then(async response => {
let json = await response.json();
result.innerHTML = JSON.stringify(json, undefined, 2);
}).catch(error => {
result.innerHTML = JSON.stringify(json, undefined, 2);
});
});
Si todo ha ido bien verás algo como lo siguiente:

El código del ejemplo lo tienes en mi GitHub.
¡Saludos!