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

Microsoft. O Identity.Web fornece padrões seguros para autenticação e autorização em aplicativos ASP.NET Core que se integram ao Microsoft Entra ID. Você pode personalizar muitos aspectos do comportamento de autenticação, preservando os recursos de segurança internos da biblioteca.

Identificar áreas personalizáveis

Area Opções de personalização
Configuração Todas as propriedades MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions
Eventos Eventos do OpenID Connect (OnTokenValidated, OnRedirectToIdentityProvider, etc.)
Aquisição de token IDs de correlação, parâmetros de consulta extras
Declarações Adicionar declarações personalizadas a ClaimsPrincipal
de interface do usuário Páginas de saída, comportamento de redirecionamento
Entrada Dicas de logon, dicas de domínio

Escolher um método de personalização

A tabela a seguir resume as áreas que você pode personalizar e o que cada área dá suporte.

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

  1. Configure<TOptions> – Configura as opções antes de serem usadas
  2. PostConfigure<TOptions> – Configura as 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 seção mostra como configurar as várias classes de opção de autenticação que Microsoft. O Identity.Web usa.

Entender o mapeamento de configuração

"AzureAd" seção em appsettings.json mapeia para várias classes:

Você pode usar qualquer propriedade dessas classes em sua configuração.

Padrão 1: Configurar MicrosoftIdentityOptions

O código a seguir personaliza MicrosoftIdentityOptions para habilitar o registro de PII, configurar as capacidades do cliente e ajustar os parâmetros de validação de 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();

Padrão 2: Configurar OpenIdConnectOptions (aplicativos Web)

O código a seguir personaliza OpenIdConnectOptions um aplicativo Web para definir o tipo de resposta, adicionar escopos e definir configurações de validação de cookie e 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;
});

Padrão 3: Configurar JwtBearerOptions (APIs Web)

O código a seguir personaliza JwtBearerOptions uma API Web para definir audiências válidas, mapeamentos de declaração e validação de tempo de vida 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 código a seguir configura as opções de política de cookie e autenticação de cookie para seu aplicativo, incluindo configurações de segurança e comportamento de expiração:

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 manipuladores de eventos

A autenticação OpenID Connect e JWT Bearer expõem eventos com os quais você pode interagir. Microsoft.Identity.Web configura seus próprios manipuladores de eventos, assim, você deve encadear seus manipuladores personalizados com os existentes para preservar a funcionalidade embutida.

Preservar manipuladores existentes

Ao adicionar manipuladores de eventos personalizados, sempre salve e chame o manipulador existente primeiro. O exemplo a seguir mostra as abordagens erradas e corretas.

O código a seguir incorretamente substitui o manipulador 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 a seguir é encadeado corretamente com o manipulador 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 comuns de eventos

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

O código a seguir adiciona declarações personalizadas ao ClaimsPrincipal após a validação do token em uma API Web. Ele pesquisa o departamento do usuário de um banco de dados e atribui uma função específica do aplicativo 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 código a seguir adiciona declarações personalizadas em um aplicativo Web chamando Microsoft Graph para recuperar dados de perfil de usuário adicionais 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 à solicitação de autorização

O código a seguir adiciona parâmetros de consulta personalizados à solicitação de autorização enviada ao provedor de identidade 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 a seguir lida com falhas de autenticação registrando o erro e retornando uma resposta de erro JSON personalizada:

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

Gerenciar o acesso negado

O código a seguir redireciona os usuários para uma página personalizada quando eles negam 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 token

Você pode personalizar como os tokens são adquiridos ao chamar APIs downstream passando opções para IDownstreamApi.

Usar IDownstreamApi com opções personalizadas

O código a seguir passa uma ID de correlação e parâmetros de consulta extras ao adquirir um token por meio 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

Você pode controlar onde os usuários chegam após a entrada e a saída e personalizar a experiência de saída.

Redirecionar para uma página específica após a entrada

Use o redirectUri parâmetro para enviar usuários para uma página específica depois que eles entrarem:

<!-- 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 finalização de sessão

Opção 1: substituir a página razor

Crie um arquivo Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml com 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 a seguir redireciona os usuários para uma página de saída personalizada em vez do padrão:

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

Personalizar a experiência de entrada

Usar dicas de login e dicas de domínio

Simplifique a experiência de entrada preenchendo previamente nomes de usuário e direcionando os usuários para locatários Microsoft Entra específicos.

Entender pistas

Dica Propósito Exemplo
loginHint Preencher previamente o campo nome de usuário/email "user@contoso.com"
domainHint Redirecionar para a página de login do inquilino específica "contoso.com"

Aplicar padrões de sugestão

Padrão 1: baseado em controlador

O código a seguir mostra as ações do controlador para login padrão, login com uma indicação de login, indicação 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 visão

O seguinte HTML mostra links de login com diferentes configurações de sugestões.

<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 código a seguir define dinamicamente dicas com base em parâmetros de consulta e cookies durante o redirecionamento para o provedor 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

Aplicativo B2B:

// Direct to customer's tenant
domainHint = customerDomain

SaaS multilocatário:

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

Seguir as práticas recomendadas

O que fazer

1. Sempre preserve os manipuladores de eventos existentes. Salve e chame o manipulador existente antes de executar 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 rastreamento. Anexe uma ID de correlação a solicitações de aquisição de token para diagnóstico:

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

3. Validar declarações personalizadas. Verifique se as declaraçõ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. Erros de personalização de log. Encapsule a lógica personalizada em blocos try-catch e registre os erros.

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

5. Teste os caminhos de êxito e de falha. Abrange todos os cenários de autenticação em seus testes:

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

O que não fazer

1. Não ignore 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 habilite o log de Informações de Identificação Pessoal (PII) na produção:

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

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

3. Não ignore a validação de 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 codificar valores confidenciais:

//  Wrong
options.ClientSecret = "mysecret123";

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

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

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

Solucionar problemas comuns

Resolver a personalização que não está funcionando

Verificar a ordem de execução:

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi configura padrões padrão
  2. Suas Configure chamadas são executadas
  3. PostConfigure chamadas em execução (se houver)
  4. Opções são usadas

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

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

Corrigir declarações personalizadas ausentes

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

  1. O OnTokenValidated manipulador é encadeado corretamente com o manipulador existente.
  2. A autenticação é bem-sucedida antes que seu código adicione declarações.
  3. As requisições são adicionadas ao ClaimsIdentity correto.

O código a seguir registra 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 disparados corretamente

Se os eventos não estiverem sendo disparados, verifique se o middleware de autenticação e autorização está registrado na ordem correta:

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