Personalize a autenticação com a Microsoft. Identity.Web

Microsoft. O Identity.Web fornece predefinições seguras para autenticação e autorização em aplicações ASP.NET Core que se integram com o Microsoft Entra ID. Pode personalizar muitos aspetos do comportamento de autenticação enquanto preserva as funcionalidades de segurança integradas da biblioteca.

Identificar áreas personalizáveis

Area Opções de Personalização
Configuração Todas MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions propriedades
Events Eventos OpenID Connect (OnTokenValidated, OnRedirectToIdentityProvider, etc.)
Aquisição de Tokens IDs de correlação, parâmetros extra de consulta
Claims Adicionar reivindicações personalizadas a ClaimsPrincipal
UI(Interface de Utilizador) Páginas de encerramento de sessão, comportamento de redirecionamento
Iniciar sessão Dicas de login, dicas de domínio

Escolha um método de personalização

A tabela seguinte resume as áreas que pode personalizar e o que cada área suporta.

Use uma de duas abordagens para personalizar as opções:

  1. Configure<TOptions> - Configura as opções antes de serem usadas
  2. PostConfigure<TOptions> - Configura opções após todas as Configure chamadas

Ordem de execução:

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

Configurar opções de autenticação

Esta secção mostra como configurar as várias classes de opções de autenticação que a Microsoft. O Identity.Web utiliza.

Compreender o mapeamento de configuração

A "AzureAd" secção em appsettings.json corresponde a múltiplas classes:

Pode usar qualquer propriedade destas classes na sua configuração.

Padrão 1: Configurar MicrosoftIdentityOptions

O seguinte código personaliza MicrosoftIdentityOptions para permitir o registo de PII, definir capacidades do cliente e ajustar parâmetros de validação de tokens:

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

Padrão 2: Configurar OpenIdConnectOptions (aplicações Web)

O código seguinte personaliza OpenIdConnectOptions uma aplicação web para definir o tipo de resposta, adicionar escopos e configurar as definições de validação de cookies e tokens:

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

Padrão 3: Configurar JwtBearerOptions (APIs Web)

O seguinte código personaliza JwtBearerOptions uma API web para definir audiências válidas, mapeamentos de reivindicações e validação vitalícia do 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
});

O seguinte código configura a política de cookies e as opções de autenticação de cookies para a sua aplicação, incluindo definições de segurança e comportamento de validade:

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

Personalizar os gestores de eventos

O OpenID Connect e a autenticação JWT Bearer expõem eventos aos quais você pode se conectar. Microsoft.Identity.Web configura os seus próprios manipuladores de eventos, por isso deve encadear os seus manipuladores existentes com os personalizados para preservar a funcionalidade incorporada.

Preservar os manipuladores existentes

Quando adicionas gestores de eventos personalizados, guarda sempre e chama primeiro o gestor existente. O exemplo seguinte mostra as abordagens erradas e corretas.

O código seguinte incorretamente sobrescreve o handler 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;
    };
});

O código seguinte encadeia corretamente com o handler existente:

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

Aplicar cenários de eventos comuns

Adicionar reivindicações personalizadas após validação do token

O código seguinte adiciona reivindicações personalizadas ao ClaimsPrincipal após a validação do token numa API web. Procura o departamento do utilizador a partir de uma base de dados e atribui um papel específico da aplicação com base no domínio de email:

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

O seguinte código adiciona reivindicações personalizadas numa aplicação web ao ligar para a Microsoft Graph para recuperar dados adicionais de perfil de utilizador após a validação do 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 ?? ""));
    };
});

Adicionar parâmetros de consulta ao pedido de autorização

O seguinte código adiciona parâmetros personalizados de consulta ao pedido de autorização enviado ao fornecedor de identidade da 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"];
        }
    };
});

Personalizar o tratamento de falhas de autenticação

O código seguinte trata das falhas de autenticação registando o erro e retornando uma resposta personalizada de erro JSON:

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

Acesso ao handle negado

O seguinte código redireciona os utilizadores para uma página personalizada quando recusam o consentimento:

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

Personalizar a aquisição de tokens

Pode personalizar a forma como os tokens são adquiridos ao chamar APIs a jusante passando opções para IDownstreamApi.

Use o IDownstreamApi com opções personalizadas

O código seguinte passa um ID de correlação e parâmetros adicionais de consulta ao adquirir um token através de 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);
    }
}

Personalizar a interface do usuário

Pode controlar onde os utilizadores são direcionados após o início e o término de sessão e personalizar a experiência após o término de sessão.

Redirecionar para uma página específica após iniciar sessão

Use o redirectUri parâmetro para enviar os utilizadores para uma página específica depois de iniciarem sessão:

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

Personalizar a página de logout

Opção 1: Sobrepor a página do Razor

Crie um ficheiro em Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml com o seu conteúdo personalizado:

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

Opção 2: Redirecionar para uma página personalizada

O código seguinte redireciona os utilizadores para uma página personalizada de saída em vez da predefinida:

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

Personalize a experiência de início de sessão

Usa dicas de login e dicas de domínio

Simplifique a experiência de início de sessão pré-preenchendo nomes de utilizador e direcionando os utilizadores para tenants específicos do Microsoft Entra.

Compreender as pistas

Sugestão Purpose Exemplo
loginHint Prepreencher o campo de nome de utilizador/email "user@contoso.com"
domainHint Página de login direta para inquilinos específicos "contoso.com"

Aplicar padrões de dicas

Padrão 1: Baseado em controlador

O código seguinte mostra as ações do controlador para iniciar sessão padrão, iniciar sessão com uma dica de login, dica de domínio, ou ambos:

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

Padrão 2: Baseado em visualizações

O seguinte HTML mostra links de início de sessão com diferentes configurações de dicas:

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

Padrão 3: Programático com OnRedirectToIdentityProvider

O seguinte código define dinamicamente pistas com base em parâmetros de consulta e cookies durante o redirecionamento para o fornecedor de identidade:

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

Casos de uso

Plataforma de Comércio Eletrónico:

// Pre-fill returning customer email
loginHint = customerEmail

Aplicação B2B:

// Direct to customer's tenant
domainHint = customerDomain

SaaS Multi-Inquilino:

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

Siga as melhores práticas

Coisas a Fazer

1. Preserve sempre os gestores de eventos existentes. Guarde e chame o handler existente antes de executar a sua lógica personalizada:

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

2. Use IDs de correlação para rastreio. Anexe um ID de correlação aos pedidos de aquisição de tokens para diagnóstico:

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

3. Validar reivindicações personalizadas. Verifique se as reivindicações personalizadas contêm valores esperados antes de conceder acesso:

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

4. Registar erros de personalização. Envolva a lógica personalizada em blocos try-catch e registe os erros.

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

5. Testar tanto os caminhos de sucesso como de fracasso. Cubra todos os cenários de autenticação nos seus testes:

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

Coisas a não fazer

1. Não ignores os manipuladores de eventos do 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ão ative o registro de PII em produção:

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

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

3. Não ignore a validação do 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. Não codifique de forma fixa valores sensíveis:

//  Wrong
options.ClientSecret = "mysecret123";

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

5. Não modifique a autenticação em middleware:

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

Resolver problemas comuns

A personalização da resolução não tem efeito

Verifique a ordem de execução:

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi Define os padrões
  2. As tuas Configure chamadas funcionam
  3. PostConfigure Chamadas em curso (se houver)
  4. São usadas opções

Solução: Utilize PostConfigure se a sua chamada Configure não estiver a ter efeito, porque PostConfigure é executada após todas as chamadas Configure:

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

Corrigir declarações personalizadas em falta

Verifique o seguinte caso não apareçam reclamações personalizadas:

  1. O OnTokenValidated handler está corretamente encadeado com o handler existente.
  2. A autenticação é bem-sucedida antes de o seu código adicionar declarações.
  3. As reivindicações são adicionadas ao ClaimsIdentity correto.

O código seguinte regista todas as declarações para depuração:

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

Corrigir eventos que não são acionados

Se os eventos não estiverem a disparar, verifique se o middleware de autenticação e autorização está registado na ordem correta:

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