Personnalisez l’authentification avec Microsoft. Identity.Web

Microsoft. Identity.Web fournit des valeurs par défaut sécurisées pour l’authentification et l’autorisation dans ASP.NET Core applications qui s’intègrent à Microsoft Entra ID. Vous pouvez personnaliser de nombreux aspects du comportement d’authentification tout en préservant les fonctionnalités de sécurité intégrées de la bibliothèque.

Identifier les zones personnalisables

Domaine Options de personnalisation
Configuration Toutes MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions propriétés
Événements Événements OpenID Connect (OnTokenValidated, OnRedirectToIdentityProvider, etc.)
Acquisition de jetons ID de corrélation, paramètres de requête supplémentaires
Revendications Ajouter des revendications personnalisées à ClaimsPrincipal
Interface Utilisateur (UI) Pages de déconnexion, comportement de redirection
Connexion Indicateurs de connexion, indicateurs de domaine

Choisir une méthode de personnalisation

Le tableau suivant récapitule les zones que vous pouvez personnaliser et ce que chaque zone prend en charge.

Utilisez l’une des deux approches pour personnaliser les options :

  1. Configure<TOptions> - Configure les options avant qu’elles ne soient utilisées
  2. PostConfigure<TOptions> - Configure les options après tous les Configure appels

Ordre d’exécution :

Configure → Configure → ... → PostConfigure → PostConfigure → ... → Options used

Configurer les options d’authentification

Cette section montre comment configurer les différentes classes d’option d’authentification qui Microsoft. Identity.Web utilise.

Comprendre le mappage de configuration

La section "AzureAd" dans appsettings.json se mappe à plusieurs classes.

Vous pouvez utiliser n’importe quelle propriété de ces classes dans votre configuration.

Modèle 1 : Configurer MicrosoftIdentityOptions

Le code suivant est personnalisé pour activer la journalisation des informations personnelles, définir les fonctionnalités du client et ajuster les paramètres de validation des jetons MicrosoftIdentityOptions :

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

// Customize Microsoft Identity options
builder.Services.Configure<MicrosoftIdentityOptions>(options =>
{
    // Enable PII logging (development only!)
    options.EnablePiiLogging = true;

    // Custom client capabilities
    options.ClientCapabilities = new[] { "CP1", "CP2" };

    // Override token validation parameters
    options.TokenValidationParameters.ValidateLifetime = true;
    options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);
});

var app = builder.Build();

Modèle 2 : Configurer OpenIdConnectOptions (applications web)

Le code suivant personnalise OpenIdConnectOptions pour une application web afin de définir le type de réponse, d’ajouter des scopes et de configurer les paramètres de validation des cookies et des jetons.

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

// Customize OpenIdConnect options
builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    // Override response type
    options.ResponseType = "code id_token";

    // Add extra scopes
    options.Scope.Add("offline_access");
    options.Scope.Add("profile");

    // Customize token validation
    options.TokenValidationParameters.NameClaimType = "preferred_username";
    options.TokenValidationParameters.RoleClaimType = "roles";

    // Set redirect URI
    options.CallbackPath = "/signin-oidc";

    // Configure cookie options
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;
});

Modèle 3 : Configurer JwtBearerOptions (API web)

Le code suivant personnalise JwtBearerOptions pour une API web afin de définir des audiences valides, des mappages de revendications et une validation valide de la durée de vie des jetons :

using Microsoft.AspNetCore.Authentication.JwtBearer;

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

// Customize JWT Bearer options
builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme,
    options =>
{
    // Customize audience validation
    options.TokenValidationParameters.ValidAudiences = new[]
    {
        "api://your-api-client-id",
        "https://your-api.com"
    };

    // Set custom claim mappings
    options.TokenValidationParameters.NameClaimType = "name";
    options.TokenValidationParameters.RoleClaimType = "roles";

    // Customize token validation
    options.TokenValidationParameters.ValidateLifetime = true;
    options.TokenValidationParameters.ClockSkew = TimeSpan.Zero; // No tolerance
});

Le code suivant configure les options d’authentification des cookies et de stratégie de cookie pour votre application, notamment les paramètres de sécurité et le comportement d’expiration :

using Microsoft.AspNetCore.Authentication.Cookies;

// Configure cookie policy
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.MinimumSameSitePolicy = SameSiteMode.Lax;
    options.Secure = CookieSecurePolicy.Always;
    options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always;
});

// Configure cookie authentication options
builder.Services.Configure<CookieAuthenticationOptions>(
    CookieAuthenticationDefaults.AuthenticationScheme,
    options =>
{
    options.Cookie.Name = "MyApp.Auth";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.ExpireTimeSpan = TimeSpan.FromHours(1);
    options.SlidingExpiration = true;
});

Personnaliser les gestionnaires d’événements

L’authentification OpenID Connect et JWT Bearer exposent les événements auxquels vous pouvez vous connecter. Microsoft. Identity.Web configure ses propres gestionnaires d’événements. Vous devez donc chaîner vos gestionnaires personnalisés avec les gestionnaires existants pour conserver les fonctionnalités intégrées.

Conserver les gestionnaires existants

Lorsque vous ajoutez des gestionnaires d’événements personnalisés, enregistrez toujours et appelez d’abord le gestionnaire existant. L’exemple suivant montre les approches incorrectes et correctes.

Le code suivant incorrectement remplace le gestionnaire Microsoft.Identity.Web :

services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
    options.Events.OnTokenValidated = async context =>
    {
        // Your code - but you LOST the built-in validation!
        await Task.CompletedTask;
    };
});

Le code suivant enchaîne correctement au gestionnaire existant :

services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
    var existingOnTokenValidatedHandler = options.Events.OnTokenValidated;

    options.Events.OnTokenValidated = async context =>
    {
        // Call Microsoft.Identity.Web's handler FIRST
        await existingOnTokenValidatedHandler(context);

        // Then your custom code
        // (executes AFTER built-in security checks)
        var identity = context.Principal.Identity as ClaimsIdentity;
        identity?.AddClaim(new Claim("custom_claim", "custom_value"));
    };
});

Appliquer des scénarios d’événements courants

Ajouter des revendications personnalisées après la validation du jeton

Le code suivant ajoute des claims personnalisés à ClaimsPrincipal après la validation du jeton dans une API web. Il recherche le service de l’utilisateur à partir d’une base de données et attribue un rôle spécifique à l’application en fonction du domaine de messagerie :

using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Security.Claims;

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme,
    options =>
{
    var existingHandler = options.Events.OnTokenValidated;

    options.Events.OnTokenValidated = async context =>
    {
        // Preserve built-in validation
        await existingHandler(context);

        // Add custom claims
        var identity = context.Principal.Identity as ClaimsIdentity;

        // Example: Add department claim from database
        var userObjectId = context.Principal.FindFirst("oid")?.Value;
        if (!string.IsNullOrEmpty(userObjectId))
        {
            var department = await GetUserDepartment(userObjectId);
            identity?.AddClaim(new Claim("department", department));
        }

        // Example: Add application-specific role
        var email = context.Principal.FindFirst("email")?.Value;
        if (email?.EndsWith("@admin.com") == true)
        {
            identity?.AddClaim(new Claim(ClaimTypes.Role, "SuperAdmin"));
        }
    };
});

Le code suivant ajoute des revendications personnalisées dans une application web en appelant Microsoft Graph pour récupérer des données de profil utilisateur supplémentaires après la validation du jeton :

using Microsoft.AspNetCore.Authentication.OpenIdConnect;

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    var existingHandler = options.Events.OnTokenValidated;

    options.Events.OnTokenValidated = async context =>
    {
        // Preserve built-in processing
        await existingHandler(context);

        // Call Microsoft Graph to get additional user data
        var graphClient = context.HttpContext.RequestServices
            .GetRequiredService<GraphServiceClient>();

        var user = await graphClient.Me.GetAsync();

        var identity = context.Principal.Identity as ClaimsIdentity;
        identity?.AddClaim(new Claim("jobTitle", user?.JobTitle ?? ""));
        identity?.AddClaim(new Claim("department", user?.Department ?? ""));
    };
});

Ajouter des paramètres de requête à la demande d’autorisation

Le code suivant ajoute des paramètres de requête personnalisés à la demande d’autorisation envoyée au fournisseur d’identité Microsoft Entra :

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    var existingHandler = options.Events.OnRedirectToIdentityProvider;

    options.Events.OnRedirectToIdentityProvider = async context =>
    {
        // Preserve existing behavior
        if (existingHandler != null)
        {
            await existingHandler(context);
        }

        // Add custom query parameters
        context.ProtocolMessage.Parameters.Add("slice", "testslice");
        context.ProtocolMessage.Parameters.Add("custom_param", "custom_value");

        // Conditional parameters based on request
        if (context.HttpContext.Request.Query.ContainsKey("prompt"))
        {
            context.ProtocolMessage.Prompt = context.HttpContext.Request.Query["prompt"];
        }
    };
});

Personnaliser la gestion des échecs d’authentification

Le code suivant gère les échecs d’authentification en journalisant l’erreur et en retournant une réponse d’erreur JSON personnalisée :

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    options.Events.OnAuthenticationFailed = async context =>
    {
        // Log the error
        var logger = context.HttpContext.RequestServices
            .GetRequiredService<ILogger<Program>>();
        logger.LogError(context.Exception, "Authentication failed");

        // Customize error response
        context.Response.StatusCode = 401;
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync($$"""
            {
                "error": "authentication_failed",
                "error_description": "{{context.Exception.Message}}"
            }
            """);

        context.HandleResponse(); // Suppress default error handling
    };
});

Gérer l’accès refusé

Le code suivant redirige les utilisateurs vers une page personnalisée lorsqu’ils refusent le consentement :

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    options.Events.OnAccessDenied = async context =>
    {
        // User denied consent
        context.Response.Redirect("/Home/AccessDenied");
        context.HandleResponse();
        await Task.CompletedTask;
    };
});

Personnaliser l’acquisition de jetons

Vous pouvez personnaliser la façon dont les jetons sont acquis lors de l’appel d’API en aval en passant des options à IDownstreamApi.

Utiliser IDownstreamApi avec des options personnalisées

Le code suivant transmet un ID de corrélation et des paramètres de requête supplémentaires lors de l’acquisition d’un jeton via IDownstreamApi:

using Microsoft.Identity.Abstractions;

public class TodoListController : ControllerBase
{
    private readonly IDownstreamApi _downstreamApi;

    public TodoListController(IDownstreamApi downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult> GetTodo(int id, Guid correlationId)
    {
        var result = await _downstreamApi.GetForUserAsync<Todo>(
            "TodoListService",
            options =>
            {
                options.RelativePath = $"api/todolist/{id}";

                // Customize token acquisition
                options.TokenAcquisitionOptions = new TokenAcquisitionOptions
                {
                    CorrelationId = correlationId,
                    ExtraQueryParameters = new Dictionary<string, string>
                    {
                        { "slice", "test_slice" }
                    }
                };
            });

        return Ok(result);
    }
}

Personnaliser l’interface utilisateur

Vous pouvez contrôler l’emplacement des utilisateurs après la connexion et la déconnexion, et personnaliser l’expérience de déconnexion.

Rediriger vers une page spécifique après la connexion

Utilisez le redirectUri paramètre pour envoyer des utilisateurs à une page spécifique après leur connexion :

<!-- Razor view -->
<a href="/MicrosoftIdentity/Account/SignIn?redirectUri=/Dashboard">Sign In</a>

<!-- Or in controller -->
[HttpGet]
public IActionResult SignInToDashboard()
{
    return RedirectToAction("SignIn", "Account", new
    {
        area = "MicrosoftIdentity",
        redirectUri = "/Dashboard"
    });
}

Personnaliser la page de déconnexion

Option 1 : Remplacer la page Razor

Créez un fichier à l'emplacement Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml avec votre contenu personnalisé :

@page
@model Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Pages.Account.SignedOutModel
@{
    ViewData["Title"] = "Signed out";
}

<div class="container text-center mt-5">
    <h1>You have been signed out</h1>
    <p>Thank you for using our application.</p>
    <a asp-area="" asp-controller="Home" asp-action="Index" class="btn btn-primary">
        Return to Home
    </a>
</div>

Option 2 : Rediriger vers une page personnalisée

Le code suivant redirige les utilisateurs vers une page de déconnexion personnalisée au lieu de la valeur par défaut :

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    options.Events.OnSignedOutCallbackRedirect = context =>
    {
        context.Response.Redirect("/Home/SignedOut");
        context.HandleResponse();
        return Task.CompletedTask;
    };
});

Personnaliser l’expérience de connexion

Utiliser des indicateurs de connexion et des indicateurs de domaine

Simplifiez l’expérience de connexion en préremplissant les noms d’utilisateur et en dirigeant les utilisateurs vers des locataires Microsoft Entra spécifiques.

Comprendre les indicateurs

Indicateur Objectif Exemple
loginHint Préremplir le champ nom d’utilisateur/e-mail "user@contoso.com"
domainHint Diriger vers la page de connexion spécifique du locataire "contoso.com"

Appliquer des modèles d'indices

Modèle 1 : basé sur le contrôleur

Le code suivant montre les actions du contrôleur pour la connexion standard, la connexion avec un indicateur de connexion, un indicateur de domaine ou les deux :

using Microsoft.AspNetCore.Mvc;

public class AuthController : Controller
{
    [HttpGet]
    public IActionResult SignIn()
    {
        // Standard sign-in
        return RedirectToAction("SignIn", "Account", new
        {
            area = "MicrosoftIdentity",
            redirectUri = "/Dashboard"
        });
    }

    [HttpGet]
    public IActionResult SignInWithLoginHint()
    {
        // Pre-populate username
        return RedirectToAction("SignIn", "Account", new
        {
            area = "MicrosoftIdentity",
            redirectUri = "/Dashboard",
            loginHint = "user@contoso.com"
        });
    }

    [HttpGet]
    public IActionResult SignInWithDomainHint()
    {
        // Direct to Contoso tenant
        return RedirectToAction("SignIn", "Account", new
        {
            area = "MicrosoftIdentity",
            redirectUri = "/Dashboard",
            domainHint = "contoso.com"
        });
    }

    [HttpGet]
    public IActionResult SignInWithBothHints()
    {
        // Pre-populate AND direct to tenant
        return RedirectToAction("SignIn", "Account", new
        {
            area = "MicrosoftIdentity",
            redirectUri = "/Dashboard",
            loginHint = "user@contoso.com",
            domainHint = "contoso.com"
        });
    }
}

Modèle 2 : basé sur l’affichage

Le code HTML suivant montre les liens de connexion avec différentes configurations d’indicateur :

<div class="sign-in-options">
    <h2>Sign In Options</h2>

    <!-- Standard sign-in -->
    <a href="/MicrosoftIdentity/Account/SignIn?redirectUri=/Dashboard"
       class="btn btn-primary">
        Sign In
    </a>

    <!-- With login hint -->
    <a href="/MicrosoftIdentity/Account/SignIn?redirectUri=/Dashboard&loginHint=user@contoso.com"
       class="btn btn-secondary">
        Sign In as user@contoso.com
    </a>

    <!-- With domain hint -->
    <a href="/MicrosoftIdentity/Account/SignIn?redirectUri=/Dashboard&domainHint=contoso.com"
       class="btn btn-secondary">
        Sign In (Contoso)
    </a>
</div>

Modèle 3 : Programmatique avec OnRedirectToIdentityProvider

Le code suivant définit dynamiquement des indicateurs en fonction des paramètres de requête et des cookies pendant la redirection vers le fournisseur d’identité :

builder.Services.Configure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options =>
{
    var existingHandler = options.Events.OnRedirectToIdentityProvider;

    options.Events.OnRedirectToIdentityProvider = async context =>
    {
        if (existingHandler != null)
        {
            await existingHandler(context);
        }

        // Add hints based on application logic
        if (context.HttpContext.Request.Query.TryGetValue("tenant", out var tenant))
        {
            context.ProtocolMessage.DomainHint = tenant;
        }

        // Get suggested user from cookie or session
        var suggestedUser = context.HttpContext.Request.Cookies["LastSignedInUser"];
        if (!string.IsNullOrEmpty(suggestedUser))
        {
            context.ProtocolMessage.LoginHint = suggestedUser;
        }
    };
});

Cas d’utilisation

Plateforme de commerce électronique :

// Pre-fill returning customer email
loginHint = customerEmail

Application B2B :

// Direct to customer's tenant
domainHint = customerDomain

SaaS multilocataire :

// Route based on subdomain
domainHint = GetTenantFromSubdomain(Request.Host)

Suivre les bonnes pratiques

Choses à faire

1. Conservez toujours les gestionnaires d’événements existants. Enregistrez et appelez le gestionnaire existant avant d’exécuter votre logique personnalisée :

var existingHandler = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
    await existingHandler(context); // Call Microsoft.Identity.Web's handler
    // Your custom code
};

2. Utilisez les ID de corrélation pour le suivi. Attachez un ID de corrélation aux demandes d’acquisition de jetons pour les diagnostics :

var tokenOptions = new TokenAcquisitionOptions
{
    CorrelationId = Activity.Current?.Id ?? Guid.NewGuid()
};

3. Valider les revendications personnalisées. Vérifiez que les revendications personnalisées contiennent des valeurs attendues avant d’accorder l’accès :

var department = context.Principal.FindFirst("department")?.Value;
if (!IsValidDepartment(department))
{
    throw new UnauthorizedAccessException("Invalid department");
}

4. Journaliser les erreurs de personnalisation. Encapsuler la logique personnalisée dans des blocs try-catch et enregistrer les erreurs.

try
{
    // Custom logic
}
catch (Exception ex)
{
    logger.LogError(ex, "Custom authentication logic failed");
    throw;
}

5. Testez les chemins de réussite et d’échec. Couvrez tous les scénarios d’authentification dans vos tests :

// Test with valid tokens
// Test with missing claims
// Test with expired tokens
// Test with wrong audience

Choses à ne pas faire

1. Ne passez pas à côté des gestionnaires d'événements de Microsoft.Identity.Web :

//  Wrong - loses built-in security checks
options.Events.OnTokenValidated = async context => { /* your code */ };

//  Correct - preserves security
var existing = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
    await existing(context);
    /* your code */
};

2. N’activez pas la journalisation des informations personnelles en production :

//  Wrong
options.EnablePiiLogging = true; // In production!

//  Correct
if (builder.Environment.IsDevelopment())
{
    options.EnablePiiLogging = true;
}

3. Ne pas contourner la validation des jetons :

//  Wrong - insecure!
options.TokenValidationParameters.ValidateLifetime = false;
options.TokenValidationParameters.ValidateAudience = false;

//  Correct - maintain security
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);

4. Ne codez pas en dur les valeurs sensibles :

//  Wrong
options.ClientSecret = "mysecret123";

//  Correct
options.ClientSecret = builder.Configuration["AzureAd:ClientSecret"];

5. Ne modifiez pas l’authentification dans le middleware :

//  Wrong - configure in Startup, not middleware
app.Use(async (context, next) =>
{
    // Modifying auth options here is too late!
});

Résoudre les problèmes courants

Résoudre le problème de personnalisation qui ne prend pas effet

Vérifier l’ordre d’exécution :

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi définit les valeurs par défaut
  2. Vos Configure appels sont en cours
  3. PostConfigure appels exécutés (le cas échéant)
  4. Les options sont utilisées

Solution: Utilisez PostConfigure si votre Configure appel ne prend pas effet, car PostConfigure s’exécute après tous les Configure appels :

services.PostConfigure<OpenIdConnectOptions>(
    OpenIdConnectDefaults.AuthenticationScheme,
    options => { /* your changes */ }
);

Corriger les revendications personnalisées manquantes

Vérifiez ce qui suit si les revendications personnalisées n’apparaissent pas :

  1. Le OnTokenValidated gestionnaire est chaîné correctement avec le gestionnaire existant.
  2. L’authentification réussit avant que votre code ajoute des revendications.
  3. Les réclamations sont ajoutées au ClaimsIdentity correct.

Le code suivant consigne toutes les demandes à des fins de débogage :

var claims = context.Principal.Claims.ToList();
logger.LogInformation($"Claims count: {claims.Count}");
foreach (var claim in claims)
{
    logger.LogInformation($"{claim.Type}: {claim.Value}");
}

Correction des événements qui ne se déclenchent pas

Si les événements ne se déclenchent pas, vérifiez que le middleware d'authentification et d'autorisation sont enregistrés dans l'ordre correct :

app.UseAuthentication(); // Must be first
app.UseAuthorization();  // Must be second
app.MapControllers();    // Then endpoints