Personalice la autenticación con Microsoft. Identity.Web

Microsoft. Identity.Web proporciona valores predeterminados seguros para la autenticación y autorización en ASP.NET Core aplicaciones que se integran con Microsoft Entra ID. Puede personalizar muchos aspectos del comportamiento de autenticación al tiempo que conserva las características de seguridad integradas de la biblioteca.

Identificación de áreas personalizables

Area Opciones de personalización
Configuración Todas las MicrosoftIdentityOptions, OpenIdConnectOptions, JwtBearerOptions propiedades
Eventos Eventos OpenID Connect (OnTokenValidated, OnRedirectToIdentityProvider, etc.)
Adquisición de tokens Identificadores de correlación, parámetros de consulta adicionales
Notificaciones Adición de declaraciones personalizadas a ClaimsPrincipal
interfaz de usuario Páginas de cierre de sesión, comportamiento de redirección
Inicio de sesión Sugerencias de inicio de sesión, sugerencias de dominio

Elección de un método de personalización

En la tabla siguiente se resumen las áreas que puede personalizar y qué admite cada área.

Use uno de estos dos enfoques para personalizar las opciones:

  1. Configure<TOptions> - Configura las opciones antes de que se usen.
  2. PostConfigure<TOptions> - Configura las opciones después de todas las Configure llamadas.

Orden de ejecución:

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

Configuración de las opciones de autenticación

En esta sección se muestra cómo configurar las distintas clases de opción de autenticación que Microsoft. Identity.Web usa.

Entender la asignación de configuración

La "AzureAd" sección en appsettings.json se asigna a varias clases.

Puede usar cualquier propiedad de estas clases en la configuración.

Patrón 1: Configurar MicrosoftIdentityOptions

El siguiente código personaliza MicrosoftIdentityOptions para habilitar el registro de PII, establecer capacidades del cliente y ajustar los parámetros de validación 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();

Patrón 2: Configurar OpenIdConnectOptions (aplicaciones web)

El código siguiente personaliza OpenIdConnectOptions para que una aplicación web establezca el tipo de respuesta, agregue ámbitos y configure las opciones de validación de cookies y 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;
});

Patrón 3: Configuración de JwtBearerOptions (API web)

El código siguiente personaliza JwtBearerOptions para que una API web establezca audiencias válidas, mapeo de reclamaciones y validación del tiempo de vida del 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
});

El código siguiente configura la directiva de cookies y las opciones de autenticación de cookies para la aplicación, incluida la configuración de seguridad y el comportamiento de expiración:

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

Personalización de controladores de eventos

La autenticación OpenID Connect y JWT Bearer expone eventos a los que puede conectarse. Microsoft. Identity.Web configura sus propios controladores de eventos, por lo que debe encadenar los controladores personalizados con los existentes para conservar la funcionalidad integrada.

Conservar los controladores existentes

Al agregar controladores de eventos personalizados, guarde y llame primero al controlador existente. En el ejemplo siguiente se muestran los enfoques incorrectos y correctos.

El siguiente código sobrescribe incorrectamente el manejador de 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;
    };
});

El código siguiente se encadena correctamente con el controlador 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"));
    };
});

Aplicación de escenarios de eventos comunes

Adición de reclamaciones personalizadas después de la validación del token

El código siguiente agrega reclamaciones personalizadas al ClaimsPrincipal después de la validación de tokens en una API web. Busca el departamento del usuario desde una base de datos y asigna un rol específico de la aplicación en función del dominio de correo electrónico:

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

El código siguiente agrega notificaciones personalizadas en una aplicación web llamando a Microsoft Graph para recuperar datos de perfil de usuario adicionales después de la validación 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 ?? ""));
    };
});

Adición de parámetros de consulta a la solicitud de autorización

El código siguiente agrega parámetros de consulta personalizados a la solicitud de autorización enviada al proveedor de identidades de 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"];
        }
    };
});

Personalización del control de errores de autenticación

El código siguiente controla los errores de autenticación registrando el error y devolviendo una respuesta de error 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
    };
});

Control del acceso denegado

El código siguiente redirige a los usuarios a una página personalizada cuando deniegan el consentimiento:

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

Personalización de la adquisición de tokens

Puede personalizar cómo se adquieren los tokens al llamar a las API de bajada pasando opciones a IDownstreamApi.

Uso de IDownstreamApi con opciones personalizadas

El código siguiente pasa un identificador de correlación y parámetros de consulta adicionales cuando se adquiere un token a travé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 la interfaz de usuario

Puede controlar dónde llegan los usuarios después del inicio de sesión y cierre de sesión y personalizar la experiencia de cierre de sesión.

Redireccionamiento a una página específica después del inicio de sesión

Use el redirectUri parámetro para enviar usuarios a una página específica después de iniciar sesión:

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

Personalización de la página de cierre de sesión

Opción 1: Invalidar la página de Razor

Cree un archivo en Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml con el contenido 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>

Opción 2: Redirigir a una página personalizada

El código siguiente redirige a los usuarios a una página de cierre de sesión personalizada en lugar del valor predeterminado:

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

Personalización de la experiencia de inicio de sesión

Uso de sugerencias de inicio de sesión y sugerencias de dominio

Optimice la experiencia de inicio de sesión rellenando previamente los nombres de usuario y dirija a los usuarios a inquilinos de Microsoft Entra específicos.

Entender las sugerencias

Sugerencia propósito Ejemplo
loginHint Rellenar previamente el campo nombre de usuario o correo electrónico "user@contoso.com"
domainHint Redirigir a la página de inicio de sesión de un inquilino específico. "contoso.com"

Aplicar patrones de sugerencia

Patrón 1: basado en controlador

En el código siguiente se muestran las acciones del controlador para el inicio de sesión estándar, el inicio de sesión con una sugerencia de inicio de sesión, una sugerencia de dominio o ambas:

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

Patrón 2: basado en vistas

En el código HTML siguiente se muestran vínculos de inicio de sesión con diferentes configuraciones de sugerencias:

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

Patrón 3: Programación con OnRedirectToIdentityProvider

El código siguiente establece de forma dinámica sugerencias basadas en parámetros de consulta y cookies durante la redirección al proveedor de identidades:

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 comercio electrónico:

// Pre-fill returning customer email
loginHint = customerEmail

Aplicación B2B:

// Direct to customer's tenant
domainHint = customerDomain

SaaS multiusuario:

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

Seguimiento de los procedimientos recomendados

Qué hacer

1. Conserve siempre los controladores de eventos existentes. Guarde y llame al controlador existente antes de ejecutar la 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 identificadores de correlación para el seguimiento. Adjunte un identificador de correlación a las solicitudes de adquisición de tokens para diagnósticos:

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

3. Valide las reclamaciones personalizadas. Compruebe que las declaraciones personalizadas contienen valores esperados antes de conceder acceso:

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

4. Errores de personalización del registro. Encapsular la lógica personalizada en bloques try-catch y registrar errores:

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

5. Pruebe tanto los caminos de éxito como los de fallo. Cubre todos los escenarios de autenticación en tus pruebas.

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

Cosas que no debes hacer

1. No omitas los controladores de eventos de Microsoft.Identity.Web:

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

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

2. No habilite el registro de PII en producción:

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

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

3. No omita la validación de tokens:

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

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

4. No codifique de forma rígida los valores confidenciales:

//  Wrong
options.ClientSecret = "mysecret123";

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

5. No modifique la autenticación en middleware:

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

Solucionar problemas comunes

Solucionar la falta de aplicación de la personalización

Compruebe el orden de ejecución:

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi establece los valores predeterminados
  2. Tus Configure llamadas se ejecutan
  3. PostConfigure llamadas que se ejecutan (si las hay)
  4. Se usan opciones

Solución: Use PostConfigure si la Configure llamada no surte efecto, porque PostConfigure se ejecuta después de todas las Configure llamadas:

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

Corregir los atributos personalizados que faltan

Compruebe lo siguiente si las declaraciones personalizadas no aparecen:

  1. El OnTokenValidated controlador se encadena correctamente con el controlador existente.
  2. La autenticación se realiza correctamente antes de que el código agregue declaraciones.
  3. Las reclamaciones se agregan al correcto ClaimsIdentity.

El código siguiente registra todas las afirmaciones para la depuración.

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

Corregir eventos que no se activan

Si los eventos no se activan, verifique que los middlewares de autenticación y autorización estén registrados en el orden correcto:

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