J'ai récemment eu besoin de mettre en place une authentification via Azure AD dans une application web ReactJS. Globalement, la procédure est assez simple, mais il y a tout de même quelques subtilités sur lesquelles j'ai perdu pas mal de temps et que je souhaiterais partager ;)

Côté client, c'est la librairie ADAL.js (Active Directory Authentication Library for Javascript) qui va nous aider. Microsoft supporte de nombreuses plateformes différentes en fonction des environnements client ou serveur visés (plus d'informations sur ce lien). Celle-ci utilise le point de terminaison Azure AD v1.0.

Microsoft propose désormais aussi la librairie MSAL (Microsoft Identity Platform Authentication Libraries, ici dans sa version Javascript) qui va utiliser le point de terminaison Azure AD v2.0. Elle apporte plusieurs améliorations par rapport à ADAL :

  • En plus des comptes professionnels, il est possible de s'authentifier avec un compte personnel (hotmail.com, outlook.com…) ou avec un compte de réseau social (Facebook, Google, LinkedIn…).
MSAL vs ADAL - Mode d'authentification MSAL vs ADAL - Mode d'authentification
  • On peut utiliser un seul ID d'application pour plusieurs plateformes. Il n'est donc plus nécessaire de créer une application par projet !
  • L'arrivée du consentement dynamique et incrémentiel. Autrement dit, cela permet à l'application de demander des autorisations supplémentaires uniquement au moment où elle en a besoin. L'expérience utilisateur est donc plus agréable (en particulier quand l'application requiert de nombreuses autorisations).

Vous trouverez de quoi approfondir le sujet ADAL vs MSAL dans la documentation Microsoft.

Enregistrement de l'application dans l'Azure AD

La 1ère étape à réaliser est de créer une application d'entreprise dans votre Azure Active Directory via le portail Azure. Globalement, la procédure est assez simple et bien documentée, mais il convient tout de même de faire attention à quelques points :

  • La bonne configuration des urls de redirection. Dans mon cas, j'en ai configuré deux : une pour les tests en local lors du développement et une seconde pour le déploiement sur Azure. Il est important que ces urls correspondent strictement à ce qui est configuré côté application cliente (cf. le paragraphe sur l'implémentation ADAL.js).
Azure - URI de redirection Azure - URI de redirection
  • Ajouter les permissions nécessaires. Pour l'authentification, le minimum est d'ajouter la permission “User.Read” du groupe “Azure Active Directory Graph". Dans l'exemple ci-dessous, j'en ai ajouté d'autres pour interagir avec l'API Microsoft Graph. Il en existe énormément pour accéder aux différents services de Microsoft (Yammer, SharePoint, Skype, OneNote, Office 365, Azure…).
Azure - Permissions Azure - Permissions
  • Activer le flux OAuth 2.0 via le fichier manifest de l'application. C'est un flux en 2 étapes qui est parfaitement adapté pour les applications web de type SPA et c'est l'approche utilisée par ADAL.js.
    • La 1ère étape consiste à envoyer les informations de configuration pour réclamer un code d’autorisation (sur le endpoint /oauth2/authorize). En cas de succès, l'utilisateur est invité à saisir ses informations de connexion et à autoriser l'application.
    • La 2nd étape consiste à récupérer un token pour accéder à la ressource désirée (sur le endpoint /oauth2/token).
    • Ce token peut ensuite être mis en cache par le client pendant toute sa durée de vie et réutilisé à chaque requête.
    • Ce processus est très bien décrit sur la documentation Microsoft.
"oauth2AllowImplicitFlow": true

Authentification via ADAL.js dans une application cliente ReactJS

Une fois notre application déclarée dans l'Azure AD, on peut désormais mettre en place l'authentification des utilisateurs côté client. Pour ReactJS, il existe un module dédié à ADAL. Celui-ci permet, entre autres, d'éviter les refresh de page lors du renouvellement du token d'authentification. Il est bien évidemment possible d'utiliser la librairie ADAL.js “brute”.

Encore une fois, la mise en place est assez simple, mais nécessite de passer par plusieurs étapes :

  • Installation du module :)
npm install react-adal
  • Création du fichier de configuration :
import { AuthenticationContext, adalFetch, withAdalLogin } from 'react-adal';

// Configuration d'ADAL
export const adalConfig = {
  tenant: "my-tenant.onmicrosoft.com", // id ou url du tenant Azure
  clientId: "00000000-0000-0000-0000-000000000000", // id de l'application créé ci-dessus
  endpoints: {
    api: "https://my-tenant.onmicrosoft.com/00000000-0000-0000-0000-000000000000"
  }, 
  redirectUri: "http://localhost:1234/signin-oidc", // url de redirection
  postLogoutRedirectUri: "http://localhost:1234", // url appelée lors de la déconnexion
  cacheLocation: 'localStorage'
};

export const authContext = new AuthenticationContext(adalConfig);

// Méthode de récupération du token
export const getToken = () => {
  var cachedToken = authContext.getCachedToken(authContext.config.clientId);
  if(cachedToken == undefined) {    
    authContext.acquireToken(authContext.config.clientId);
  }
  else {
    return cachedToken;
  }
};

// Méthode permettant d'exécuter un appel via la librairie ADAL
export const adalApiFetch = (fetch, url, options) =>
  adalFetch(authContext, adalConfig.endpoints.api, fetch, url, options);
  • Appel de la méthode d'authentification au démarrage de l'application :
import { runWithAdal } from 'react-adal';
import { authContext } from './adalConfig';

const DO_NOT_LOGIN = false;

runWithAdal(authContext, () => {
  ...
  ReactDOM.render(
    <App />
  );
  ...
},DO_NOT_LOGIN);
  • On peut récupérer le token d'authentification via la méthode getToken(), pour ensuite le passer dans les en-têtes des requêtes HTTP (vers une API par exemple) :
const authLink = setContext((_, { headers }) => {
    // Get the authentication token 
    const token = getToken();
  
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : "",
      }
    }
  });
  • On peut profiter de ce token pour y lire certaines informations de l'utilisateur (nom et prénom, email, id et rôles par exemple) :
// Decode token for get user informations
const user = decodeJWT(getToken());

Dans le cas d'une application ReactJS hébergée dans un IIS, il est nécessaire de rajouter un fichier web.config permettant de gérer la redirection virtuelle vers l'url configurée dans le fichier. Sans celui-ci, l'authentification ne fonctionnera pas et vous aurez un magnifique message d'erreur !

Le message est pourtant limpide... Le message est pourtant limpide...
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="Redirect" stopProcessing="true">
                    <match url="signin-oidc*" />
                    <action type="Rewrite" url="/" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>

Authentification Azure AD dans une API ASP.Net Core

Pour être complet, je vais rapidement aborder la partie serveur de l'authentification Azure AD. On va vu ci-dessus que l'on pouvait envoyer notre token à une API via JWT (~Bearer Token). Il va être nécessaire d'ajouter quelques lignes de code pour prendre en compte cela et valider les tokens envoyés (et aussi permettre de savoir qui est l'utilisateur derrière la requête ;) ).

La travail s'opère dans la classe Startup.cs au moment de l'injection du mécanisme d'authentification :

// Add authentication (Azure AD)
services
   .AddAuthentication(options =>
   {
      options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
   })
   .AddJwtBearer(options =>
   {
      options.Audience = Configuration["AzureAd:ClientId"];
      options.Authority = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}";
      options.Events = new JwtBearerEvents
      {
         OnTokenValidated = context =>
         {
            // Check if the user has an OID claim
            if (!context.Principal.HasClaim(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier"))
            {
               context.Fail($"The claim 'oid' is not present in the token.");
            }

            ClaimsPrincipal userPrincipal = context.Principal;
            var oid = userPrincipal.Claims.First(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").Value;

            // TODO...            
            return Task.CompletedTask;
         },
         OnAuthenticationFailed = context =>
         {
            // TODO...
            return Task.CompletedTask;
         }
   };
});

// Ne pas oublier d'ajouter app.UseAuthentication(); dans la méthode Configure() 

On a à disposition 2 évènements permettant d'intercepter le cas d'un token valide et le cas où l'authentification a échoué.

Voilà, vous avez désormais les billes pour mettre en place ce type de scénario d'authentification dans vos applications. Vous avez pu remarquer que c'était globalement assez simple et rapide, il est donc dommage de s'en passer !