Authentification Azure AD simple avec une SPA dans une application aspnetcore
Il y a quelques mois, mon cher collègue Jonathan nous a fait une petite présentation en interne des différents workflows d'authentification d'une SPA sur une API aspnetcore via OpenId Connect, et l'une des conclusions de sa présentation me reste particulièrement en tête :
Au final,
l'authentification par cookies
est probablement l'une des manières les plus sécurisées de protéger une API.
Récemment, j'ai eu besoin de sécuriser une api aspnetcore
par une simple vérification d'email provenant d'Azure AD
depuis une SPA React
. En faisant quelques recherches sur le net, je fus assez surpris de ne trouver que des exemples relativement "complexes` utilisant MSAL.js entre autre.
Workflow d'authentification simple
J'ai alors appelé mon autre collègue Thomas qui m'a redirigé tout d'abord sur l'intégration built-in
d'Azure AD
dans une web app aspnetcore (qui peut être obtenue très facilement en suivant la documentation officielle). Il m'a également dit de regarder plus précisément du côté du Microsoft.AspNetCore.Authentication
middleware qui, par défaut, sécurise tous les endpoints des Controllers
et effectue une redirection implicite des utilisateurs vers la page d'authentification Azure AD
lorsque ceux-ci ne sont pas encore authentifiés.
Mais nous avons rencontré quelques problèmes avec cette solution :
- Lorsque la SPA
React
appelait l'API, elle obtenait un status code de redirection302
à la place d'une erreur401
. - L'authentification par défaut ne vérifie que si l'utilisateur est bien présent dans l'annuaire
Azure AD
mais sans aucune autre vérification (i.e. que son email était bien présent dans notre base de données)
Du coup, toujours avec l'aide de Thomas, nous avons réussi à avoir un workflow plutôt "simple" pour authentifier nos utilisateurs SPA :
- Lorsque l'utilisateur essaie d'accéder à une API sans être authentifié, il récupère une erreur
401
. - Lorsque l'app
React
récupère une erreur401
, on redirige l'utilisateur vers un endpoint d'authentification appelé/api/auth
par exemple. - Cet endpoint se charge de rediriger l'utilisateur vers la page de login
Azure AD
et appelle de nouveau l'endpoint/api/auth
lorsque l'authentification est réussie et permet la génération d'un cookie d'authentification. - Lorsque l'utilisateur est accède à l'endpoint
/api/auth
en étant authentifié, on vérifie son email et l'on redirige l'utilisateur vers la page principale. - Par la suite, les autres appels aux API sont faits avec le
Cookie d'authentification
et peuvent se passer sans aucun souci.
Afin de mettre en place ce workflow, nous avons dû effectuer les quelques modifications suivantes :
- Au lieu de rediriger les utilisateurs vers la page de login
Azure AD
en cas d'accès à un endpoint d'API sans être authentifié, on renvoie une erreur401
. - On configure l'authentification
Azure AD
pour que la vérification se fasse parCookie
(et ainsi permettre à la SPA de n'avoir "qu'à transmettre` le cookie d'authentification pour accéder aux API). - Dans la SPA, appeler l'endpoint
/api/auth
pour s'authentifier surAzure AD
en cas d'erreur401
- Dans le même endpoint d'API, vérifier l'email d'utilisateur et le déconnecter (par exemple) en cas d'accès non autorisé.
Surcharge de la redirection Azure AD par une erreur 401
Afin de réaliser ce scénario, nous avons défini comme DefaultChallengeScheme
un CustomApiScheme
, ainsi qu'un CustomApiAuthenticationHandler
afin de retourner une erreur 401
au lieu d'une redirection :
private static readonly string CustomApiScheme = "CustomApiScheme"; private void ConfigureAuthentication(IServiceCollection services) { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = AzureADDefaults.CookieScheme; options.DefaultChallengeScheme = CustomApiScheme; options.DefaultSignInScheme = AzureADDefaults.CookieScheme; }) .AddAzureAD(options => { Configuration.Bind("AzureAd", options); }) .AddScheme<AuthenticationSchemeOptions, CustomApiAuthenticationHandler>(CustomApiScheme, options => { }); ... }
public class CustomApiAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public CustomApiAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task HandleChallengeAsync(AuthenticationProperties properties) { Response.StatusCode = 401; return Task.CompletedTask; } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { return Task.FromResult(AuthenticateResult.NoResult()); } }
Utilisation deCookies
pour l'authentification Azure AD
Rien d'exceptionnel ici, juste la configuration des CookieAuthenticationOptions
en utilisant le AzureADDefaults.CookieScheme
ainsi que les propriétés que l'on souhaite que notre Cookie ait.
private void ConfigureAuthentication(IServiceCollection services) { ... services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options => { options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Lax; }); ...
Redirection vers l'endpoint /api/auth
en cas d'erreur 401
Dans notre application d'exemple, nous n'avons qu'un simple appel API dans le fichier FetchData.js
. Dans une application plus complexe, on pourra plutôt configurer le comportement de fetch
, axios
ou tout autre client http pour implémenter ce comportement.
async populateWeatherData() { ... if (response.status == 401) { // Redirect to authentication point if not authorized window.location.href = "/api/auth"; } ... }
Déclenchement de l'authentification Azure AD dans notre endpoint
Pour cette dernière partie, nous avons besoin d'un endpoint accessible de manière anonyme qui pourra permettre de déclencher le Challenge(AzureADDefaults.OpenIdScheme)
en cas d'accès sans être authentifié. Cela provoque la redirection de l'utilisateur vers la mire de login Azure AD, rappelle cet endpoint (configuré en tant que ReplyUrl
sur Azure AD) et permet la création du cookie d'authentification. Il ne nous reste plus qu'à rediriger notre utilisateur vers notre SPA.
[Route("api/[controller]")] [ApiController] public class AuthController : ControllerBase { [HttpGet] [AllowAnonymous] public IActionResult Login(CancellationToken cancellationToken = default) { if (User.Identity.IsAuthenticated) // = Is User authenticated by Azure AD { try { // Check if user access is legitimate on this website, and throw UnauthorizedAccessException if not } catch (UnauthorizedAccessException) { return Unauthorized(new { Message = "You are not authorized to access to this platform." }); } } else { // Trigger Azure AD authentication (using redirection) return Challenge(AzureADDefaults.OpenIdScheme); } // Redirect to home page is successfully authenticated return Redirect("/"); } }
Commentaires additionnels
Je fus assez surpris de ne pas réussir à trouver de mise en place simple d'une authentification par cookie avec Azure AD, une SPA et aspnetcore.
Un point important à noter est que ce scénario nécessite que la SPA soit hébergée sur le même domaine que l'API (Ce qui est le fonctionnement par défaut lors de la création d'un projet via dotnet new react
ou dotnet new angular
), étant donné que les Cookies ont une portée par Domain
et Path
.
J'espère que cet article vous aura aidé ou vous aura permis de découvrir une autre manière de sécuriser votre API avec Azure AD.
N'hésitez pas à manifester vos remarques ou toute opinion dans les commentaires ou sur mon compte Twitter @vivienfabing. Que le code soit avec vous !
Note : vous pourrez trouver un exemple sur mon GitHub.
Commentaires