Actualizar access tokens cuando usas Implicit Flow

Los tokens que el servidor de autorización otorga, después de una autenticación correcta del usuario, tienen un tiempo de expiración. Sin embargo, si los usuarios tuvieran que estar continuamente autenticándose sería una pésima experiencia para ellos. Con el objetivo de que esto no ocurra existen los que se llaman refresh tokens. Estos son tan valiosos como un secreto, ya que pueden ser intercambiados por un access token directamente. Sin embargo, estos no están disponibles en todos los flujos que ofrecen OAuth 2.0 por motivos de seguridad, ya que si estos son expuestos en un cliente público (de los que no son capaces de guardar secretos) podrían facilitar a cualquiera recuperar tokens de acceso.

En el último artículo estuve hablando de las aplicaciones en JavaScript e Implicit Flow. Este es uno de los flujos que no soporta los refresh tokens. Sin embargo, existe una técnica llamada Silent Refresh que nos permite actualizar el token de acceso, una vez que el usuario se ha autenticado al menos una vez.

Muchas librerías clientes de OAuth tienen implementada esta técnica. Sin embargo, he creado una aplicación de ejemplo para que puedas ver cómo funciona.

La teoría

En realidad es muy sencillo: Se crea un iframe oculto en el cliente que hace la petición al servidor de autorización. Esta petición puede ocurrir bien cuando el token actual haya expirado, y el recurso protegido nos devuelva un 401, o a través de un temporizador. Una vez que el iframe obtenga el nuevo token actualizará el valor actual para poder seguir trabajando a los recursos protegidos.

Aplicación de ejemplo

Esta aplicación está basada en el ejemplo de Implicit Flow.

Recuperar el token de acceso a través del servidor de autorización

Una vez que hemos obtenido el token ya podremos refrescarlo a través del botón Silent refresh.

Botón Silent Refresh para refrescar el token de acceso.

Este botón está asociado a la siguiente función:

function silentRefresh() {

    const iframe_id = 'silent-refresh-iframe';

    //Remove iframe if exists
    let old_iframe = element(iframe_id);

    if (old_iframe)
        document.body.removeChild(old_iframe);

    //Create an iframe
    const iframe = document.createElement('iframe');
    iframe.id = iframe_id;

    //create login URL
    const Authorization_Endpoint = `https://login.microsoftonline.com/${config.tenantId}/oauth2/authorize?`;
    const Response_Type = config.response_type;
    const Client_Id = config.client_id;
    const Redirect_Uri = window.location.origin + '/silent-refresh.html';
    const Scope =  config.scope;
    const Resource = config.resource;
    const State = config.state;
    const Prompt = 'none';

    const url = `${Authorization_Endpoint}?response_type=${Response_Type}&client_id=${Client_Id}&redirect_uri=${Redirect_Uri}&scope=${Scope}&resource=${Resource}&state=${State}&prompt=${Prompt}`;

    iframe.setAttribute('src', url);

    //Hide iframe
    iframe.style.display = 'none';

    document.body.appendChild(iframe);
}

Primero, se comprueba si ha sido invocada anteriormente, buscando un iframe con el mismo id que se le asigna cuando se crea, y luego crea el iframe que te conté, el cual recreará la llamada al endpoint de autorización. Es importante que esta petición contenga un nuevo parámetro llamado Prompt con el valor a none, el cual evitará que el servidor de autorización muestre ninguna página de autenticación o de consentimiento. Si el usuario no está todavía autenticado o es necesario aceptar algún consentimiento la petición devolverá un error.
Además, para este ejemplo he utilizado una redirect uri diferente a la URL original, la cual obtiene el primer token. Si bien se podría utilizar la misma, he preferido que esta sea una distinta para evitar la carga de recursos innecesarios dentro del iframe. En este caso es importante que ambas URLS estén dadas de altas en la aplicación registrada, en el servidor de autorización.

La página silent-refresh.html, a la que será redirigida esta petición de refresco, solamente contiene lo siguiente:

<!doctype html>
<html>
<body>
    <script>
         parent.postMessage(location.hash, location.origin);
    </script>
</body></html>

Básicamente, al cargar la página, lo que hará será enviar un mensaje al padre con el hash recibido, donde estará el nuevo token, por lo que es necesario recuperar dicho mensaje para poder reemplazar nuestro token expirado por este nuevo.

//Get  messages from the child (iframe)
window.addEventListener('message', (e) => {
    console.log('parent received message!:  ', e.data);

    if (e.data.includes && e.data.includes('access_token')) {

        access_token = getParameterByName('access_token', e.data);
        clearInterval(interval);
        DisplayInfo();
    }
});

El ejemplo completo lo tienes en mi GitHub.

¡Saludos!