使用 Microsoft.Identity.Web 自定义身份验证

Microsoft。Identity.Web 在与Microsoft Entra ID集成的 ASP.NET Core应用程序中提供身份验证和授权的安全默认值。 可以自定义身份验证行为的许多方面,同时保留库的内置安全功能。

确定可自定义区域

面积 自定义选项
配置 全部MicrosoftIdentityOptionsOpenIdConnectOptionsJwtBearerOptions属性
事件 OpenID Connect 事件(OnTokenValidatedOnRedirectToIdentityProvider等)
令牌获取 相关 ID、额外的查询参数
申请 将自定义声明添加到 ClaimsPrincipal
用户界面 用户注销页面,重定向操作行为
登录 登录提示、域提示

选择自定义方法

下表总结了可以自定义的区域以及每个区域支持的内容。

使用以下两种方法之一自定义选项:

  1. Configure<TOptions> - 在使用之前配置选项
  2. PostConfigure<TOptions> - 在所有 Configure 调用后配置选项

执行顺序:

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

配置身份验证选项

本部分展示了如何配置 Microsoft.Identity.Web 使用的各种身份验证选项类。

了解配置映射

"AzureAd" 节中的 appsettings.json 映射到多个类:

可以在配置中使用这些类中的任何属性。

模式 1:配置 MicrosoftIdentityOptions

以下代码自定义 MicrosoftIdentityOptions 以启用 PII 日志记录、设置客户端功能并调整令牌验证参数:

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

模式 2:配置 OpenIdConnectOptions (Web 应用)

以下代码定制 OpenIdConnectOptions Web 应用,以设置响应类型、添加作用域并配置 Cookie 和令牌验证设置:

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

模式 3:配置 JwtBearerOptions (Web API)

以下代码自定义 JwtBearerOptions Web API ,用于设置有效的受众、声明映射及令牌存续期验证:

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

以下代码为应用配置 Cookie 策略和 Cookie 身份验证选项,包括安全设置和过期行为:

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

自定义事件处理程序

OpenID Connect 和 JWT 持有者身份验证公开可挂钩的事件。 Microsoft。Identity.Web 设置自己的事件处理程序,因此必须将自定义处理程序与现有处理程序链接,以保留内置功能。

保留现有处理程序

添加自定义事件处理程序时,请始终先保存并调用现有处理程序。 以下示例显示了错误和正确的方法。

以下代码不正确覆盖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;
    };
});

以下代码与现有处理程序 正确 链式调用:

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

应用常见事件方案

在令牌验证后添加自定义声明

以下代码将在验证令牌后,将自定义声明添加到 ClaimsPrincipal 的 Web API 中。 它从数据库中查找用户的部门,并根据电子邮件域分配应用程序特定角色。

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

以下代码通过在令牌验证后调用 Microsoft Graph,检索额外的用户配置文件数据,从而在 Web 应用中添加自定义声明:

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

将查询参数添加到授权请求

以下代码将自定义查询参数添加到发送到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"];
        }
    };
});

自定义身份验证失败处理

以下代码通过记录错误并返回自定义 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
    };
});

处理拒绝访问

以下代码在用户拒绝同意时将用户重定向到自定义页面:

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

自定义令牌获取

可以通过将选项传递给IDownstreamApi来自定义调用下游 API 时获取令牌的方法。

将 IDownstreamApi 与自定义选项配合使用

以下代码在通过IDownstreamApi 获取令牌时传递相关性 ID 和额外的查询参数:

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

自定义 UI

可以控制用户在登录和注销后的到达页面,并自定义用户注销后的体验。

登录后重定向到特定页面

redirectUri使用参数在用户登录后将用户发送到特定页面:

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

自定义已退出页面

选项 1:覆盖 Razor 页面

Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml 创建一个包含您自定义内容的文件。

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

选项 2:重定向到自定义页面

以下代码将用户重定向到自定义注销页面,而不是默认页面:

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

自定义登录体验

使用登录提示和域提示

通过预先填充用户名并将用户定向到特定Microsoft Entra租户来简化登录体验。

了解提示

提示 Purpose 示例
loginHint 预填充用户名/电子邮件字段 "user@contoso.com"
domainHint 直接到特定租户登录页 "contoso.com"

应用提示模式

模式 1:基于控制器

以下代码显示了标准登录、使用登录提示、域提示或两者登录的控制器操作:

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

模式 2:基于视图

以下 HTML 显示了具有不同提示配置的登录链接:

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

模式 3:使用 OnRedirectToIdentityProvider 编程

以下代码在重定向到身份提供者的过程中,根据查询参数和 Cookie 动态设置提示:

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

用例

电子商务平台:

// Pre-fill returning customer email
loginHint = customerEmail

B2B 应用程序:

// Direct to customer's tenant
domainHint = customerDomain

多租户 SaaS:

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

遵循最佳做法

可执行的操作

1.始终保留现有事件处理程序。 在运行自定义逻辑之前保存并调用现有处理程序:

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

2. 使用相关性 ID 进行跟踪。 将关联 ID 附加到令牌获取请求的诊断中:

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

3.验证自定义声明。 在授予访问权限之前,验证自定义声明是否包含预期值:

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

记录自定义错误日志。 在 try-catch 块中包装自定义逻辑,并记录错误:

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

5. 测试成功路径和失败路径。 涵盖测试中的所有身份验证方案:

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

禁忌事项

1.不要跳过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.不要在生产环境中启用 PII 日志记录:

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

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

3. 不要绕过令牌验证:

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

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

4.不要硬编码敏感值:

//  Wrong
options.ClientSecret = "mysecret123";

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

5. 不要在中间件中修改身份验证:

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

排查常见问题

解决自定义未生效问题

检查执行顺序:

  1. AddMicrosoftIdentityWebApp / AddMicrosoftIdentityWebApi 设置默认值
  2. 你的 Configure 调用正在运行
  3. PostConfigure 调用运行(如果有)
  4. 选项被使用

解决方案:如果Configure调用未生效,请使用PostConfigure,因为PostConfigure在所有Configure调用后才会运行:

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

修复缺少的自定义声明

如果未显示自定义声明,请验证以下内容:

  1. 处理程序 OnTokenValidated 与现有处理程序正确链接。
  2. 在代码添加声明之前,身份验证会成功。
  3. 声明被添加到正确的 ClaimsIdentity

以下代码将所有声明记录下来以便调试:

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

修复未触发的事件

如果未触发事件,请验证身份验证和授权中间件是否按正确的顺序注册:

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