Personalizzare l'autenticazione con Microsoft. Identity.Web

Microsoft. Identity.Web offre impostazioni predefinite sicure per l'autenticazione e l'autorizzazione nelle applicazioni ASP.NET Core che si integrano con Microsoft Entra ID. È possibile personalizzare molti aspetti del comportamento di autenticazione mantenendo al tempo stesso le funzionalità di sicurezza predefinite della libreria.

Identificare le aree personalizzabili

Area Opzioni di personalizzazione
Configuration Tutte le MicrosoftIdentityOptions, OpenIdConnectOptions e JwtBearerOptions proprietà
Events Eventi OpenID Connect (OnTokenValidated, OnRedirectToIdentityProvidere così via)
Acquisizione di token ID di correlazione, parametri di query aggiuntivi
Claims Aggiungere attestazioni personalizzate a ClaimsPrincipal
UI Pagine di disconnessione, comportamento di reindirizzamento
Accesso Suggerimenti di login, suggerimenti di dominio

Scegliere un metodo di personalizzazione

La tabella seguente riepiloga le aree che è possibile personalizzare e ciò che ogni area supporta.

Usare uno dei due approcci per personalizzare le opzioni:

  1. Configure<TOptions> - Configura le opzioni prima di essere usate
  2. PostConfigure<TOptions> - Configura le opzioni dopo tutte le Configure chiamate

Ordine di esecuzione:

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

Configurare le opzioni di autenticazione

Questa sezione illustra come configurare le varie classi di opzioni di autenticazione Microsoft. Identity.Web usa .

Comprendere il mapping della configurazione

La sezione in "AzureAd"appsettings.json mappa a più classi:

È possibile usare qualsiasi proprietà di queste classi nella configurazione.

Modello 1: Configurare MicrosoftIdentityOptions

Il codice seguente personalizza MicrosoftIdentityOptions per consentire il log delle informazioni personali (PII), impostare le capacità del client e regolare i parametri di convalida dei token.

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();

Modello 2: Configurare OpenIdConnectOptions (app Web)

Il codice seguente consente di personalizzare OpenIdConnectOptions per un'app Web per impostare il tipo di risposta, aggiungere ambiti e configurare le impostazioni di convalida dei cookie e dei token:

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;
});

Modello 3: Configurare JwtBearerOptions (API Web)

Il codice seguente personalizza JwtBearerOptions per un'API Web impostando destinatari validi, mappature di attestazioni e convalida della durata dei token.

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
});

Il codice seguente configura i criteri di cookie e le opzioni di autenticazione dei cookie per l'app, incluse le impostazioni di sicurezza e il comportamento di scadenza:

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;
});

Personalizzare i gestori eventi

L'autenticazione OpenID Connect e JWT Bearer espongono eventi a cui è possibile collegarsi. Microsoft. Identity.Web configura i propri gestori eventi, quindi è necessario concatenare i gestori personalizzati con quelli esistenti per mantenere le funzionalità predefinite.

Mantenere i gestori esistenti

Quando si aggiungono gestori eventi personalizzati, salvare e chiamare prima il gestore esistente. Nell'esempio seguente vengono illustrati gli approcci errati e corretti.

Il codice seguente in modo non corretto sovrascrive il gestore di 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;
    };
});

Il codice seguente correttamente collega al gestore esistente:

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"));
    };
});

Applicare scenari di eventi comuni

Aggiungere attestazioni personalizzate dopo la convalida del token

Il codice seguente aggiunge attestazioni personalizzate al ClaimsPrincipal dopo la convalida del token in un'API web. Cerca il reparto dell'utente da un database e assegna un ruolo specifico dell'applicazione in base al dominio di posta elettronica:

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"));
        }
    };
});

Il codice seguente aggiunge attestazioni personalizzate in un'app Web chiamando Microsoft Graph per recuperare dati aggiuntivi del profilo utente dopo la convalida del token:

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 ?? ""));
    };
});

Aggiungere parametri di query alla richiesta di autorizzazione

Il codice seguente aggiunge parametri di query personalizzati alla richiesta di autorizzazione inviata al provider di 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"];
        }
    };
});

Personalizzare la gestione degli errori di autenticazione

Il codice seguente gestisce gli errori di autenticazione registrando l'errore e restituendo una risposta di errore JSON personalizzata:

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
    };
});

Gestione dell'accesso negato

Il codice seguente reindirizza gli utenti a una pagina personalizzata quando negano il consenso:

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;
    };
});

Personalizzare l'acquisizione di token

È possibile personalizzare la modalità di acquisizione dei token quando si chiamano le API downstream passando le opzioni a IDownstreamApi.

Usare IDownstreamApi con opzioni personalizzate

Il codice seguente passa un ID di correlazione e parametri di query aggiuntivi durante l'acquisizione di un token tramite 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);
    }
}

Personalizzare l'interfaccia utente

È possibile controllare la pagina a cui gli utenti vengono indirizzati dopo l'accesso e la disconnessione e personalizzare l'esperienza dopo la disconnessione.

Reindirizzare a una pagina specifica dopo l'accesso

Usare il redirectUri parametro per inviare gli utenti a una pagina specifica dopo l'accesso:

<!-- 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"
    });
}

Personalizzare la pagina di disconnessione

Opzione 1: Eseguire l'override della pagina Razor

Creare un file in Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml con il contenuto personalizzato:

@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>

Opzione 2: Reindirizzare a una pagina personalizzata

Il codice seguente reindirizza gli utenti a una pagina di disconnessione personalizzata anziché all'impostazione predefinita:

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

Personalizzare l'esperienza di accesso

Usare hint di accesso e hint di dominio

Ottimizzare l'esperienza di accesso popolando automaticamente i nomi utente e indirizzando gli utenti verso tenant specifici di Microsoft Entra.

Comprendere i suggerimenti

Suggerimento Scopo Esempio
loginHint Precompilare il campo nome utente/indirizzo di posta elettronica "user@contoso.com"
domainHint Pagina di accesso diretto a un tenant specifico "contoso.com"

Applicare modelli di hint

Modello 1: basato su controller

Il codice seguente mostra le azioni del controller per l'accesso standard, l'accesso con un hint di accesso, un hint di dominio o entrambi:

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"
        });
    }
}

Modello 2: basato sulla visualizzazione

Il codice HTML seguente mostra i collegamenti di accesso con configurazioni di hint diverse:

<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>

Modello 3: a livello di codice con OnRedirectToIdentityProvider

Il codice seguente imposta in modo dinamico gli hint in base ai parametri di query e ai cookie durante il reindirizzamento al provider di 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;
        }
    };
});

Casi d'uso

Piattaforma di e-commerce:

// Pre-fill returning customer email
loginHint = customerEmail

Applicazione B2B:

// Direct to customer's tenant
domainHint = customerDomain

SaaS multi-tenant:

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

Seguire le migliori pratiche

Cose da fare

1. Mantenere sempre i gestori eventi esistenti. Salvare e chiamare il gestore esistente prima di eseguire la logica personalizzata:

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

2. Usare gli ID di correlazione per il tracciamento. Associare un ID di correlazione alle richieste di acquisizione di token per la diagnostica:

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

3. Convalidare le attestazioni personalizzate. Verificare che le attestazioni personalizzate contengano valori previsti prima di concedere l'accesso:

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

4. Registrare gli errori di personalizzazione. Avvolgere la logica personalizzata in blocchi try-catch e registrare gli errori:

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

5. Testare sia i percorsi di esito positivo che quello di errore. Coprire tutti gli scenari di autenticazione nei test:

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

Cose da non fare

1. Non saltare i gestori di eventi di 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. Non abilitare la registrazione delle informazioni personali nell'ambiente di produzione:

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

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

3. Non ignorare la convalida dei token:

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

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

4. Non codificare valori sensibili:

//  Wrong
options.ClientSecret = "mysecret123";

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

5. Non modificare l'autenticazione nel middleware:

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

Risolvere i problemi comuni

Risolvere la mancata applicazione della personalizzazione

Controllare l'ordine di esecuzione:

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi imposta le impostazioni predefinite
  2. Le Configure chiamate vengono eseguite
  3. PostConfigure chiamate eseguite (se presenti)
  4. Vengono usate le opzioni

Soluzione: Usare PostConfigure se la Configure chiamata non ha effetto, perché PostConfigure viene eseguita dopo tutte le Configure chiamate:

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

Correzione di attestazioni personalizzate mancanti

Verificare quanto segue se le attestazioni personalizzate non vengono visualizzate:

  1. Il OnTokenValidated gestore viene concatenato correttamente con il gestore esistente.
  2. L'autenticazione ha esito positivo prima che il codice aggiunga i claim.
  3. Le dichiarazioni vengono aggiunte all'entità corretta ClaimsIdentity.

Il codice seguente registra tutte le attestazioni per il debug:

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

Correzione degli eventi non attivati

Se gli eventi non si attivano, verificare che il middleware di autenticazione e quello di autorizzazione siano registrati nell'ordine corretto:

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