OpenIddict 中文指南
什么是 OpenIddict?
OpenIddict 是一个 开源且多功能的框架,用于在任何 ASP.NET Core 2.1(及更高版本)和传统 ASP.NET 4.6.1(及更高版本)应用程序中构建符合标准的 OAuth 2.0/OpenID Connect 服务器 。
OpenIddict 诞生于 2015 年末,最初基于 AspNet.Security.OpenIdConnect.Server(代号为 ASOS),这是一个受微软为 OWIN 项目开发的 OAuth 2.0 授权服务器中间件以及首个为 ASP.NET Core 创建的 OpenID Connect 服务器启发的低级 OpenID Connect 服务器中间件。
到 2020 年,ASOS 被合并入 OpenIddict 3.0,形成了一个统一在 OpenIddict 伞下的堆栈,同时仍为新用户提供易于使用的途径,并通过一种“降级模式”为高级用户提供低级体验,该模式允许以无状态方式使用 OpenIddict(即不依赖后端数据库)。
作为这一进程的一部分,OpenIddict 3.0 添加了对 Microsoft.Owin
的原生支持,以便在传统的 ASP.NET 4.6.1(及更高版本)应用程序中使用它,使得在不必迁移到 ASP.NET Core 的情况下替换 OAuthAuthorizationServerMiddleware
和 OAuthBearerAuthenticationMiddleware
成为可能。
核心概念
用户认证
与其他解决方案不同,OpenIddict 专门关注授权过程中的 OAuth 2.0/OpenID Connect 协议方面,并将用户认证留给实现者处理:OpenIddict 可以与任何形式的用户认证无缝配合,如密码、令牌、联合身份验证或集成 Windows 身份验证。虽然方便,但使用如 ASP.NET Core Identity 这样的会员资格框架并非必需。
与 OpenIddict 的集成通常通过启用传递模式来完成,该模式在控制器操作或最小 API 处理程序中处理请求,或者对于更复杂场景,直接利用其高级事件模型。
传递模式
与 OAuthAuthorizationServerMiddleware
类似,OpenIddict 允许在自定义控制器操作或其他能够插入 ASP.NET Core 或 OWIN 请求处理管道的中间件中处理授权、注销和令牌请求。在这种情况下,OpenIddict 会首先验证传入的请求(例如,确保必需的参数存在且有效),然后再调用管道的其余部分:如果发生任何验证错误,OpenIddict 将自动拒绝请求,防止其到达用户定义的控制器操作或自定义中间件。
builder.Services.AddOpenIddict()
.AddServer(options =>
{
// Enable the authorization and token endpoints.
options.SetAuthorizationEndpointUris("/authorize")
.SetTokenEndpointUris("/token");
// Enable the authorization code flow.
options.AllowAuthorizationCodeFlow();
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the authorization endpoint
// to allow the /authorize minimal API handler to handle authorization requests
// after being validated by the built-in OpenIddict server event handlers.
//
// Token requests will be handled by OpenIddict itself by reusing the identity
// created by the /authorize handler and stored in the authorization codes.
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough();
});
app.MapGet("/authorize", async (HttpContext context) =>
{
// Resolve the claims stored in the principal created after the Steam authentication dance.
// If the principal cannot be found, trigger a new challenge to redirect the user to Steam.
var principal = (await context.AuthenticateAsync(SteamAuthenticationDefaults.AuthenticationScheme))?.Principal;
if (principal is null)
{
return Results.Challenge(properties: null, [SteamAuthenticationDefaults.AuthenticationScheme]);
}
var identifier = principal.FindFirst(ClaimTypes.NameIdentifier)!.Value;
// Create a new identity and import a few select claims from the Steam principal.
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType);
identity.AddClaim(new Claim(Claims.Subject, identifier));
identity.AddClaim(new Claim(Claims.Name, identifier).SetDestinations(Destinations.AccessToken));
return Results.SignIn(new ClaimsPrincipal(identity), properties: null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
});
事件模型
OpenIddict 为其服务器和验证堆栈实现了一个强大的基于事件的模型:请求处理逻辑的每一部分都实现为事件处理器,这些处理器可以被移除、移动到管道中的不同位置,或被自定义处理器替换,以便覆盖 OpenIddict 使用的默认逻辑:
/// <summary>
/// Contains the logic responsible of rejecting authorization requests that don't specify a valid prompt parameter.
/// </summary>
public class ValidatePromptParameter : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseSingletonHandler<ValidatePromptParameter>()
.SetOrder(ValidateNonceParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Reject requests specifying prompt=none with consent/login or select_account.
if (context.Request.HasPrompt(Prompts.None) && (context.Request.HasPrompt(Prompts.Consent) ||
context.Request.HasPrompt(Prompts.Login) ||
context.Request.HasPrompt(Prompts.SelectAccount)))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6040));
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2052(Parameters.Prompt),
uri: SR.FormatID8000(SR.ID2052));
return default;
}
return default;
}
}
在 OpenIddict 本身中,事件处理器通常定义为专用类,但也可以使用委托进行注册:
services.AddOpenIddict()
.AddServer(options =>
{
options.AddEventHandler<HandleConfigurationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
// Attach custom metadata to the configuration document.
context.Metadata["custom_metadata"] = 42;
return default;
}));
});