项目
版本

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 的情况下替换 OAuthAuthorizationServerMiddlewareOAuthBearerAuthenticationMiddleware 成为可能。

核心概念

用户认证

与其他解决方案不同,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;
            }));
    });
在本文档中