Utilizando Azure AD para implementar OAuth2 en una API mediante las cuentas corporativas de tu empresa

Motivación del artículo

El otro día me encargaron una tarea de la que debía realizar una investigación... En nuestra empresa tenemos una serie de aplicaciones internas, que solo puede utilizar gente que tenga cuenta corporativa de la empresa. En nuestro caso, éstas son cuentas de Microsoft (Office365), por lo que tenía bastante sentido que, en lugar de implementar nosotros un login propio para estas aplicaciones, o utilizar herramientas externas como Keycloak, utilizáramos las propias cuentas de la empresa para iniciar sesión, como cuando vas a Word o a Outlook.

Digamos que la documentación es... abrumadora. Hay mil sitios donde te explican cómo hacer esto, para mil casos de uso distintos, pero en ningún lado está toda la información centralizada (lo más parecido es este repositorio donde hay 6 proyectos cada uno con una documentación eterna para casos específicos).

Por ello, vamos a ver, paso a paso, cómo securizar con OAuth2 mediante Azure una API desarrollada en .NET 6, todo en un artículo y lo más fácil posible.

Primer paso: configurar la aplicación en Azure

Crear la aplicación en Azure

Lo primero de todo que haremos es entrar en Azure con nuestra cuenta corporativa. Una vez dentro, vamos a buscar en la barra superior App registrations. Un dato importante es que recomiendo poner la interfaz de Azure en inglés, porque los términos son un poco extraños o difíciles de traducir y seguramente sea más fácil seguirlo en inglés.

En esta parte, vamos a darle a New registration y aparecerá una ventana como esta:

Le ponemos un nombre para poder identificarla y seleccionamos Accounts in this organizational directory only , para que solo cuentas de nuestra empresa puedan acceder. En la sección de abajo seleccionamos Web y ponemos la URL de redirección tras iniciar sesión correctamente. Luego podemos añadir más URLs de redirección, y esto es un parámetro que se le pasará a las peticiones para obtener los tokens. La URL que se le pase en la petición deberá estar en esta lista.

Registramos la aplicación y vamos a la pestaña del menú de la izquierda llamada Expose an API.

Crear los scopes

Un scope define a qué recursos se puede acceder de la API. Se pueden usar, por ejemplo, si tienes una API con 3 aplicaciones cliente (3 fronts) que acceden a partes totalmente diferentes y separadas, para definir a qué recursos pueden acceder, tal y como se explica en este artículo. Nuestro caso va a ser sencillo, con una sola aplicación interna y una API, por lo que sólo vamos a crear un scope.

En la pestaña de Expose an API, lo primero es obtener una URI del ID de aplicación. Para ello le damos a Set, arriba. Luego, pulsamos en Add a scope. La ubicación de ambos botones se encuentra en la siguiente imagen:

En el scope lo más importante es poner Admins and users , el resto de la información la podemos rellenar como queramos.

Aceptar tokens de Oauth2

En el menú de la izquierda, vamos a Manifest y cambiamos el valor accessTokenAcceptedVersion a 2, como se muestra en la imagen:

Crear un secret

Lo último que necesitamos configurar es un secret para la aplicación. Para ello,m vamos a Certificates and secrets en la pestaña Client secrets y le damos a crear uno nuevo.

Segundo paso: añadir la autenticación a la API

Para añadir la autenticación a la API, añadimos lo siguiente en el fichero appsettings.json:

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<el publisher domain>",
    "TenantId": <el Id de nuestro tenant>,
    "ClientId": "<el Id de la app registration que creamos>"
}

Para saber el ClientId y el TenantId podemos ir a la pestaña Overview en el menú de la izquierda de Azure:

Para conocer el Domain podemos ir al Manifest en el menú de la izqueirda y buscar la clave PublisherDomain.

Además de esto, tendremos que añadir las siguientes líneas en el Program.cs :

builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration); // añadir la primera de todas

...

// después de app.UseRouting()
app.UseAuthentication();
app.UseAuthorization();

Tercer paso: obtener el token

Obtener un código de autorización

En primer lugar, necesitamos obtener un código de autorización. Este es el paso en el que el usuario va a introducir sus credenciales y a iniciar sesión con su cuenta corporativa.

Para ello, hay que hacer una petición GET a la siguiente URL (nota: los saltos de línea están puestos para mayor claridad, pero la URL debe ir seguida):

https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize?client_id=<client-id>
&response_type=code
&redirect_uri=<redirect-uri>
&response_mode=query
&scope=<scope> offline_access
&state=<state>
&code_challenge<code-challenge>
&code_challenge_method=S256

Lo importante de esta petición es que el parámetro redirect_uri tiene que ser una de las que definimos en el primer paso. Además, el scope debe tener el formato <id_scope>/<nombre_scope>, es decir, sin la parte de api://.

El añadir un espacio y luego offline_access al parámetro scope es para que en la respuesta cuando obtengamos el token nos devuelva también un refresh token.

El state no es más que un string que nosotros le pasamos y que puede ser lo que queramos. Este mismo state se nos devolverá en la respuesta, y así podremos evitar cross-site attacks.

El code challenge es otra especie de string de seguridad, y podemos generarlos por ejemplo en esta página.

Una vez realicemos esta petición, si todo ha salido bien, recibiremos una respuesta en forma de redirección a la siguiente URL:

<redirect-uri>/?code=<code>&state=<state>&session_state=<session-state>

En este caso, lo que nos interesa quedarnos es con el code, que es lo que Microsoft llama Authorization Code que nos va a servir para, ya sí, pedir el access token para nuestra API.

Obtener el access token y el refresh token

Una vez tenemos el authorization code, para obtener el access token (es decir, el bearer token que vamos a utilizar ya para logearnos contra nuestra API) y el refresh token, vamos a hacer un POST a la siguiente URL:

https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token

En el cuerpo, le vamos a pasar (como x-www-form-urlencoded) los siguientes parámetros:

client_id = <client id, i.e., application id>
scope = <el mismo scope que antes>
code = <el authorization code>
redirect_uri = <nuestra URL de redirección> 
grant_type = authorization_code
state = <el mismo secret que usamos antes>
code_verifier = <el code challenge que usamos antes>
client_secret = <el secret que creamos antes>

Y, si todo sale bien, recibiremos en la respuesta el access token y el refresh token.

Obtener nuevos access tokens utilizando el refresh token

Si se nos caduca el access token pero tenemos un refresh token válido, no hace falta que el usuario vuelva a iniciar sesión, sino que podemos obtener nuevos access tokens mediante el uso del refresh token.

Para esto, hacemos una nueva petición al mismo endpoint que antes:

https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token

Solo que esta vez, vamos a cambiar lo que le pasamos en el body:

client_id: <client id, i.e., application id>
grant_type: refresh_token
refresh_token: <el refresh token obtenido antes>
client_secret: <el secret que creamos antes>

Con esto, obtendremos una respuesta con un nuevo access token y refresh token.

Cuarto paso: Añadir autorización

Lo más normal es que la aplicación, aparte de tener un proceso de autenticación (o sea, login), tenga un proceso de autorización (es decir, que haya unos roles o permisos y que ciertos usuarios puedan acceder solo a aquellas cosas para las que tengan permiso). Evidentemente, esto se puede hacer también desde Azure, y, de hecho, no es muy difícil.

Para crear los roles de usuario, nos vamos a App roles en el menú de la izquierda y le damos a Create app role para todos los que queramos. Para asignarlos a usuarios de nuestra empresa, simplemente hacemos click en How do I assign app roles y en la pestaña que se nos abre le damos a Enterprise applications.

En la siguiente página, le damos a Assign users and groups:

Y ya desde ahí podemos hacer la asignación para todos los usuarios que queramos.

Por último, para securizar cualquier endpoint o controller, ponemos el típico:

[Authorize(Roles = "My role name")]
[HttpGet]
public async Task<IActionResult> Get()
{
    ...
}

De forma que solamente los usuarios que hayamos asignado al rol My role name podrán acceder a ese endpoint. El resto, recibirán un 403 (si están logeados pero no tienen permiso para ver eso) o un 401 (si no están logeados o el token no es válido).

Conclusiones

Espero que con este artículo haya quedado un poco más claro el flujo de cómo securizar una API utilizando Azure AD, porque, si bien es verdad que existe muchísima documentación, en ocasiones puede ser abrumadora y para un caso sencillo como este cuesta seguirla.