OpenIddict介绍
OpenIddict 是一个开源的通用框架 ,用于在任何 ASP.NET Core 2.1(及更高版本)和遗留 ASP.NET 4.6.1(及更高版本)应用程序中构建符合标准的 OAuth 2.0/OpenID Connect 服务器。
OpenIddict 诞生于 2015 年底,最初是基于AspNet.Security.OpenIdConnect.Server (代号 ASOS),一个低级的 OpenID Connect 服务器中间件,灵感来自微软为 OWIN 项目开发的 OAuth 2.0 授权服务器中间件和第一个 OpenID曾经为 ASP.NET Core 创建的连接服务器。
2020 年,ASOS 被合并到 OpenIddict 3.0 中,在 OpenIddict 保护伞下形成一个统一的堆栈,同时仍然为新用户提供易于使用的方法,并为高级用户提供低级体验,这得益于允许的“降级模式”以无状态方式使用 OpenIddict(即没有后备数据库)。
Microsoft.Owin
作为此过程的一部分, OpenIddict 3.0 中添加了对 的本机支持,以允许在遗留 ASP.NET 4.6.1(及更高版本)应用程序中使用它,使其成为替代OAuthAuthorizationServerMiddleware
且OAuthBearerAuthenticationMiddleware
无需迁移到 ASP.NET Core 的绝佳候选者。
核心概念
用户认证
与其他解决方案不同,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, new[] { 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;
}));
});