Microsoft。Identity.Web 在与Microsoft Entra ID集成的 ASP.NET Core应用程序中提供身份验证和授权的安全默认值。 可以自定义身份验证行为的许多方面,同时保留库的内置安全功能。
确定可自定义区域
| 面积 | 自定义选项 |
|---|---|
| 配置 | 全部MicrosoftIdentityOptions、OpenIdConnectOptions、JwtBearerOptions属性 |
| 事件 | OpenID Connect 事件(OnTokenValidatedOnRedirectToIdentityProvider等) |
| 令牌获取 | 相关 ID、额外的查询参数 |
| 申请 | 将自定义声明添加到 ClaimsPrincipal |
| 用户界面 | 用户注销页面,重定向操作行为 |
| 登录 | 登录提示、域提示 |
选择自定义方法
下表总结了可以自定义的区域以及每个区域支持的内容。
使用以下两种方法之一自定义选项:
-
Configure<TOptions>- 在使用之前配置选项 -
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
});
模式 4:配置 Cookie 选项
以下代码为应用配置 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!
});
排查常见问题
解决自定义未生效问题
检查执行顺序:
-
AddMicrosoftIdentityWebApp/AddMicrosoftIdentityWebApi设置默认值 - 你的
Configure调用正在运行 -
PostConfigure调用运行(如果有) - 选项被使用
解决方案:如果Configure调用未生效,请使用PostConfigure,因为PostConfigure在所有Configure调用后才会运行:
services.PostConfigure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme,
options => { /* your changes */ }
);
修复缺少的自定义声明
如果未显示自定义声明,请验证以下内容:
- 处理程序
OnTokenValidated与现有处理程序正确链接。 - 在代码添加声明之前,身份验证会成功。
- 声明被添加到正确的
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