Přizpůsobte ověřování pomocí Microsoft. Identity.Web

Microsoft. Identity.Web poskytuje zabezpečené výchozí hodnoty pro ověřování a autorizaci v aplikacích ASP.NET Core, které se integrují s Microsoft Entra ID. Při zachování integrovaných funkcí zabezpečení knihovny můžete přizpůsobit mnoho aspektů chování ověřování.

Identifikace přizpůsobitelných oblastí

Plocha Možnosti přizpůsobení
Configuration Všechny MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions vlastnosti
Události Události OpenID Connect (OnTokenValidated, OnRedirectToIdentityProvider atd.)
Získání tokenu ID korelace, další parametry dotazu
Claims Přidat vlastní nároky do ClaimsPrincipal
Uživatelské rozhraní Odhlášení stránek, chování přesměrování
Přihlášení Rady pro přihlášení, nápovědy k doméně

Volba metody přizpůsobení

Následující tabulka shrnuje oblasti, které můžete přizpůsobit a jaké jednotlivé oblasti podporují.

K přizpůsobení možností použijte jeden ze dvou přístupů:

  1. Configure<TOptions> – Konfiguruje možnosti před jejich používáním.
  2. PostConfigure<TOptions> – Nakonfiguruje možnosti po všech Configure voláních.

Pořadí provádění:

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

Konfigurace možností ověřování

Tato část ukazuje, jak nakonfigurovat různé třídy možností ověřování, které Microsoft. Identity.Web používá.

Principy mapování konfigurace

Sekce "AzureAd" v appsettings.json mapuje do více tříd:

V konfiguraci můžete použít libovolnou vlastnost z těchto tříd.

Vzor 1: Konfigurace MicrosoftIdentityOptions

Následující kód přizpůsobí MicrosoftIdentityOptions aktivaci protokolování PII, nastaví schopnosti klienta a upraví parametry ověřování tokenu:

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

Vzor 2: Konfigurovat OpenIdConnectOptions (webové aplikace)

Následující kód přizpůsobuje OpenIdConnectOptions pro webovou aplikaci k nastavení typu odpovědi, přidání rozsahů oprávnění a konfiguraci nastavení ověřování cookie a 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;
});

Vzorec 3: Konfigurace JwtBearerOptions (webová rozhraní API)

Následující kód přizpůsobí JwtBearerOptions pro webové API tak, aby nastavil platné publikum, mapování nároků a ověření doby platnosti 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
});

Následující kód nakonfiguruje zásady souborů cookie a možnosti ověřování souborů cookie pro vaši aplikaci, včetně nastavení zabezpečení a chování při vypršení platnosti:

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

Přizpůsobení obslužných rutin událostí

Ověřování OpenID Connect a JWT Bearer zveřejňuje události, ke kterým se můžete připojit. Microsoft. Identity.Web nastavuje vlastní obslužné rutiny událostí, takže vlastní obslužné rutiny musíte zřetězovat s existujícími obslužnými rutinami, abyste zachovali předdefinované funkce.

Zachovat současné obsluhy

Když přidáte vlastní obslužné rutiny událostí, vždy nejprve uložte a volejte existující obslužnou rutinu. Následující příklad ukazuje nesprávné a správné přístupy.

Následující kód nesprávně přepíše Microsoft.Identity.Web obslužná rutina.

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

Následující kód správně zřetědí stávající obslužnou rutinu:

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

Použití běžných scénářů událostí

Přidání vlastních deklarací identity po ověření tokenu

Následující kód přidá vlastní deklarace identity do ClaimsPrincipal po ověření tokenu ve webovém rozhraní API. Vyhledá oddělení uživatele z databáze a přiřadí roli specifickou pro aplikaci na základě e-mailové domény:

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

Následující kód přidá do webové aplikace vlastní deklarace voláním Microsoft Graph k načtení dalších dat profilu uživatele po validaci tokenu.

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

Přidání parametrů dotazu do žádosti o autorizaci

Následující kód přidá vlastní parametry dotazu do žádosti o autorizaci odeslané zprostředkovateli identity 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"];
        }
    };
});

Přizpůsobte řešení selhání ověřování

Následující kód zpracovává chyby ověřování zaznamenáním chyby a vrácením vlastní JSON odpovědi s chybou.

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

Řešení odepření přístupu

Následující kód přesměruje uživatele na vlastní stránku při odepření souhlasu:

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

Úprava procesu získání tokenu

cs-CZ: Možnost získání tokenů při volání podřízených rozhraní API můžete přizpůsobit předáním možností do IDownstreamApi.

Použití IDownstreamApi s vlastními možnostmi

Následující kód předá ID korelace a další parametry dotazu při získání tokenu prostřednictvím 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);
    }
}

Přizpůsobení uživatelského rozhraní

Můžete určit, kde se uživatelé dostanou po přihlášení a odhlášení, a přizpůsobit prostředí odhlášení.

Přesměrování na konkrétní stránku po přihlášení

Pomocí parametru redirectUri odešlete uživatele na konkrétní stránku po přihlášení:

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

Přizpůsobení stránky odhlášení

Možnost 1: Přepsání stránky Razor Page

Vytvořte soubor Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml s vlastním obsahem:

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

Možnost 2: Přesměrování na vlastní stránku

Následující kód přesměruje uživatele na vlastní odhlášenou stránku místo výchozí stránky:

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

Přizpůsobení přihlašovacího prostředí

Použití tipů pro přihlášení a nápovědy k doméně

Zjednodušte přihlašování tím, že předem vyplníte uživatelská jména a přesměrujete uživatele na konkrétní tenanti Microsoft Entra.

Vysvětlení tipů

Hint Purpose Příklad
loginHint Předplnit pole uživatelského jména a e-mailu "user@contoso.com"
domainHint Přímo na přihlašovací stránku konkrétního tenanta "contoso.com"

Použití vzorů nápovědy

Model 1: Založený na kontroleru

Následující kód ukazuje akce kontroleru pro standardní přihlášení, přihlášení pomocí nápovědy pro přihlášení, nápovědu k doméně nebo obojí:

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

Vzorec 2: Na základě zobrazení

Následující kód HTML zobrazuje přihlašovací odkazy s různými konfiguracemi nápovědy:

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

Vzor 3: Programování s OnRedirectToIdentityProvider

Následující kód dynamicky nastavuje rady na základě parametrů dotazu a souborů cookie během přesměrování na zprostředkovatele identity:

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

Případy použití

Platforma elektronického obchodování:

// Pre-fill returning customer email
loginHint = customerEmail

Aplikace B2B:

// Direct to customer's tenant
domainHint = customerDomain

SaaS s více tenanty:

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

Dodržujte osvědčené postupy.

Co dělat

1. Vždy zachovat existující obslužné rutiny událostí. Před spuštěním vlastní logiky uložte a zavolejte existující obslužnou rutinu:

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

2. Pro trasování použijte ID korelace. Připojte ID korelace k žádostem o získání tokenu pro diagnostiku:

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

3. Ověřte vlastní nároky. Před udělením přístupu ověřte, že uživatelské nároky obsahují očekávané hodnoty.

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

4. Chyby přizpůsobení logování. Zabalte vlastní logiku do bloků try-catch a zaznamenejte chyby.

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

5. Otestujte cesty úspěchu i selhání. Zahrňte všechny scénáře ověřování ve svých testech.

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

Zakázané činnosti

1. Nepřeskočte obslužné rutiny událostí 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. Nepovolujte protokolování PII v produkčním prostředí:

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

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

3. Nepoužívat ověřování 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. Neokódujte citlivé hodnoty:

//  Wrong
options.ClientSecret = "mysecret123";

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

5. Neupravujte ověřování v middlewaru:

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

Řešení běžných potíží

Vyřešit, že přizpůsobení nefunguje.

Zkontrolujte pořadí provádění:

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi nastaví výchozí hodnoty.
  2. Vaše Configure volání probíhají
  3. PostConfigure volání se spustí (pokud existuje)
  4. Používají se možnosti.

Řešení: Použijte PostConfigure , pokud se volání Configure neprojeví, protože PostConfigure se spustí po všech Configure voláních:

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

Oprava chybějících vlastních nároků

Pokud se vlastní nároky nezobrazí, ověřte následující:

  1. Obslužná rutina OnTokenValidated je správně zřetězená s existující obslužnou rutinou.
  2. Ověřování proběhne úspěšně předtím, než váš kód přidá identifikační nároky.
  3. Položky jsou přidány ke správnému ClaimsIdentity.

Následující kód zaznamenává všechny nároky pro ladění.

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

Oprava neaktivujících událostí

Pokud se události neaktivují, ověřte, že jsou ověřovací a autorizační middleware zaregistrovány ve správném pořadí.

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