L'authentification intégrée de SWA

L'authentification intégrée de SWA

L'authentification intégrée de SWA

Cet article fait partie de la série Azure Static Web Apps avec Blazor.

Un exemple complet et fonctionnel est disponible ici : mongeon/code-examples · azure-swa-blazor/04-authentication.

L’authentification, c’est souvent le bout le plus pénible à brancher dans un projet. SWA simplifie beaucoup la chose avec un système intégré qui gère le login, le logout et le profil utilisateur sans que vous ayez à écrire du code d’authentification.

Comment ça marche?

SWA expose un dossier système /.auth/ qui contient tout ce qu’il faut :

  • /.auth/login/github pour se connecter avec GitHub
  • /.auth/login/aad pour se connecter avec Microsoft Entra ID (anciennement Azure AD)
  • /.auth/logout pour se déconnecter
  • /.auth/me pour récupérer les infos de l’utilisateur connecté

Ces deux fournisseurs (GitHub et Microsoft Entra ID) sont disponibles sur le tier gratuit, sans aucune configuration. Vous n’avez pas besoin de créer une app registration, pas de client ID, pas de secret. SWA s’occupe de tout.

Si vous avez besoin d’un autre fournisseur (Auth0, Google, etc.), vous pouvez en configurer un custom, mais ça demande le tier Standard.

Ajouter le login dans Blazor

Le plus simple, c’est un lien vers le endpoint de login. Dans votre MainLayout.razor ou un composant de navigation :

<a href="/.auth/login/github">Sign in with GitHub</a>
<a href="/.auth/logout?post_logout_redirect_uri=/">Sign out</a>

Le paramètre post_logout_redirect_uri contrôle où l’utilisateur est redirigé après la déconnexion. Sans ce paramètre, il reste sur une page SWA générique.

Vous pouvez aussi rediriger l’utilisateur vers une page spécifique après le login avec post_login_redirect_uri :

<a href="/.auth/login/github?post_login_redirect_uri=/dashboard">Sign in</a>

Récupérer l’utilisateur dans Blazor

Le endpoint /.auth/me retourne un JSON avec les informations de l’utilisateur connecté. Pour l’intégrer proprement dans Blazor, on va créer un AuthenticationStateProvider custom.

D’abord, le modèle qui représente la réponse de /.auth/me :

public class ClientPrincipal
{
    public string IdentityProvider { get; set; } = string.Empty;
    public string UserId { get; set; } = string.Empty;
    public string UserDetails { get; set; } = string.Empty;
    public IEnumerable<string> UserRoles { get; set; } = [];
}

public class AuthResponse
{
    public ClientPrincipal? ClientPrincipal { get; set; }
}

Ensuite, le AuthenticationStateProvider qui appelle /.auth/me :

using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;

public class StaticWebAppsAuthenticationStateProvider : AuthenticationStateProvider
{
    private readonly HttpClient _http;

    public StaticWebAppsAuthenticationStateProvider(HttpClient http)
    {
        _http = http;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        try
        {
            var response = await _http.GetFromJsonAsync<AuthResponse>("/.auth/me");
            var principal = response?.ClientPrincipal;

            if (principal == null)
                return new AuthenticationState(new ClaimsPrincipal());

            var identity = new ClaimsIdentity(principal.IdentityProvider);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, principal.UserId));
            identity.AddClaim(new Claim(ClaimTypes.Name, principal.UserDetails));

            foreach (var role in principal.UserRoles)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, role));
            }

            return new AuthenticationState(new ClaimsPrincipal(identity));
        }
        catch
        {
            return new AuthenticationState(new ClaimsPrincipal());
        }
    }
}

On enregistre le tout dans Program.cs :

builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, StaticWebAppsAuthenticationStateProvider>();

Et on met à jour App.razor pour utiliser le CascadingAuthenticationState :

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Page not found.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Après ça, vous pouvez utiliser les composants Blazor standard <AuthorizeView> dans vos pages :

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity?.Name!
        <a href="/.auth/logout?post_logout_redirect_uri=/">Sign out</a>
    </Authorized>
    <NotAuthorized>
        <a href="/.auth/login/github">Sign in with GitHub</a>
    </NotAuthorized>
</AuthorizeView>

Récupérer l’utilisateur dans l’API

Côté Azure Functions, SWA injecte automatiquement un header x-ms-client-principal dans les requêtes vers /api/*. Ce header contient les infos de l’utilisateur en Base64.

Pour le décoder dans une fonction isolated worker :

using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Microsoft.Azure.Functions.Worker.Http;

public static class StaticWebAppsAuth
{
    public static ClaimsPrincipal ParsePrincipal(HttpRequestData req)
    {
        if (!req.Headers.TryGetValues("x-ms-client-principal", out var headerValues))
            return new ClaimsPrincipal();

        var data = headerValues.First();
        var decoded = Convert.FromBase64String(data);
        var json = Encoding.UTF8.GetString(decoded);
        var principal = JsonSerializer.Deserialize<ClientPrincipal>(json,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

        if (principal == null)
            return new ClaimsPrincipal();

        var identity = new ClaimsIdentity(principal.IdentityProvider);
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, principal.UserId));
        identity.AddClaim(new Claim(ClaimTypes.Name, principal.UserDetails));

        foreach (var role in principal.UserRoles.Where(r => r != "anonymous"))
        {
            identity.AddClaim(new Claim(ClaimTypes.Role, role));
        }

        return new ClaimsPrincipal(identity);
    }
}

Vous pouvez réutiliser le modèle ClientPrincipal en le mettant dans un projet Shared entre le Client et l’Api. C’est un des avantages d’avoir tout en C# : le même modèle sert des deux côtés.

Utilisation dans une fonction :

[Function("GetProfile")]
public async Task<HttpResponseData> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "profile")] HttpRequestData req)
{
    var user = StaticWebAppsAuth.ParsePrincipal(req);

    if (!user.Identity?.IsAuthenticated ?? true)
    {
        return req.CreateResponse(HttpStatusCode.Unauthorized);
    }

    var response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync(new
    {
        Name = user.Identity.Name,
        Roles = user.Claims
            .Where(c => c.Type == ClaimTypes.Role)
            .Select(c => c.Value)
    });

    return response;
}

Protéger des routes avec le config

On a vu dans l’article 2 comment protéger des routes dans staticwebapp.config.json. C’est la première couche de sécurité, et elle est appliquée par SWA avant que la requête arrive à votre code :

{
  "routes": [
    {
      "route": "/admin/*",
      "allowedRoles": ["authenticated"]
    }
  ],
  "responseOverrides": {
    "401": {
      "statusCode": 302,
      "redirect": "/.auth/login/github"
    },
    "404": {
      "rewrite": "/404.html"
    }
  }
}

C’est bien pour bloquer l’accès au niveau du proxy, mais validez toujours l’utilisateur dans votre API aussi. La config protège les routes, le code protège les données.

Bloquer un fournisseur

Par défaut, GitHub et Microsoft Entra ID sont activés. Si vous voulez forcer l’utilisation d’un seul fournisseur, bloquez les autres en retournant un 404 sur leur route de login :

{
  "routes": [
    {
      "route": "/.auth/login/aad",
      "statusCode": 404
    }
  ]
}

Les rôles personnalisés

SWA fournit deux rôles built-in : anonymous (tout le monde) et authenticated (les utilisateurs connectés). Vous pouvez créer des rôles personnalisés et les assigner aux utilisateurs via le portail Azure, sous Static Web Apps > Role management > Invitations.

Vous invitez un utilisateur par son identifiant de fournisseur (par exemple son username GitHub) et vous lui assignez un ou plusieurs rôles. Ces rôles apparaissent ensuite dans le ClientPrincipal et peuvent être utilisés dans la config et dans le code.

Dans le prochain article, on va explorer le CLI swa pour le développement local. C’est l’outil qui simule le proxy, le backend Functions et l’authentification sur votre machine.

Articles de la série

  1. C’est quoi une Azure Static Web App?
  2. Le fichier staticwebapp.config.json
  3. Brancher une API Azure Functions
  4. L’authentification intégrée de SWA (Cet article)
  5. Développer localement avec le CLI swa
  6. Les preview environments sur les PR

Bon déploiement, et pensez à vérifier /.auth/me dans le navigateur pour valider que votre login fonctionne.


Cet article a été rédigé avec l’aide de l’IA et révisé par moi.


Suggestions de lecture :