项目
版本

OpenIddict 授权存储

为了跟踪令牌的逻辑链和用户同意,OpenIddict 支持在数据库中存储授权(在某些 OpenID Connect 实现中也称为“授予”)。

授权类型

授权可以是两种类型之一:永久授权和临时授权。

永久授权

永久授权是由开发者定义的授权,通过使用 IOpenIddictAuthorizationManager.CreateAsync() API 创建,并使用 OpenIddict 特定的 principal.SetAuthorizationId() 扩展方法明确地附加到 ClaimsPrincipal 上。 这类授权通常用于记住用户同意,避免为每个授权请求显示同意界面。为此,可以为每个应用程序定义一个“同意类型”,例如以下示例所示:

// 从数据库中检索应用程序详情.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
    throw new InvalidOperationException("The application cannot be found.");

// 从数据库中检索与用户和应用程序关联的永久授权信息.
var authorizations = await _authorizationManager.FindAsync(
    subject: await _userManager.GetUserIdAsync(user),
    client : await _applicationManager.GetIdAsync(application),
    status : Statuses.Valid,
    type   : AuthorizationTypes.Permanent,
    scopes : request.GetScopes()).ToListAsync();

switch (await _applicationManager.GetConsentTypeAsync(application))
{
    // 如果同意是外部授予的(例如,当授权由系统管理员授予时),
    // 如果在数据库中找不到任何授权,则立即返回错误.
    case ConsentTypes.External when !authorizations.Any():
        return Forbid(
            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
            properties: new AuthenticationProperties(new Dictionary<string, string>
            {
                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                    "The logged in user is not allowed to access this client application."
            }));

    // 如果同意是隐式的,或者已经找到了一个授权,
    // 则不显示同意表单,直接返回授权响应.
    case ConsentTypes.Implicit:
    case ConsentTypes.External when authorizations.Any():
    case ConsentTypes.Explicit when authorizations.Any() && !request.HasPrompt(Prompts.Consent):
        // 创建基于声明的身份,OpenIddict将使用它来生成令牌.
        var identity = new ClaimsIdentity(
            authenticationType: TokenValidationParameters.DefaultAuthenticationType,
            nameType: Claims.Name,
            roleType: Claims.Role);

        // 向将保存在令牌中的声明添加内容.
        identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
                .SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
                .SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user))
                .SetClaims(Claims.Role, (await _userManager.GetRolesAsync(user)).ToImmutableArray());

        // 注意:在此示例中,授予的范围与请求的范围匹配,
        // 但您可能希望允许用户取消选中特定范围。
        // 若要实现此操作,在调用SetScopes之前简单地限制范围列表即可.
        identity.SetScopes(request.GetScopes());
        identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());

        // 自动创建一个永久授权,以避免将来对于包含相同范围的授权或令牌请求需要明确的同意
        var authorization = authorizations.LastOrDefault();
        authorization ??= await _authorizationManager.CreateAsync(
            identity: identity,
            subject : await _userManager.GetUserIdAsync(user),
            client  : await _applicationManager.GetIdAsync(application),
            type    : AuthorizationTypes.Permanent,
            scopes  : identity.GetScopes());

        identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
        identity.SetDestinations(static claim => claim.Type switch
        {
            // 如果授予了“profile”范围,允许将“name”声明
            // 添加到从此主体派生的访问令牌和身份令牌中.
            Claims.Name when claim.Subject.HasScope(Scopes.Profile) =>
            [
                OpenIddictConstants.Destinations.AccessToken,
                OpenIddictConstants.Destinations.IdentityToken
            ],

            // 切勿将“secret_value”声明添加到访问令牌或身份令牌中。
            // 在这种情况下,它将仅被添加到授权代码、刷新令牌和
            // 用户/设备代码中,这些代码始终是加密的
            "secret_value" => [],

            // 否则,只将该声明添加到访问令牌中.
            _ => [OpenIddictConstants.Destinations.AccessToken]
        });

        return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

    // 至此,如果在数据库中未找到任何授权,并且客户端应用程序在授权请求中指定了 prompt=none,就必须返回错误
    case ConsentTypes.Explicit   when request.HasPrompt(Prompts.None):
    case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
        return Forbid(
            authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
            properties: new AuthenticationProperties(new Dictionary<string, string>
            {
                [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
                [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
                    "Interactive user consent is required."
            }));

    // 在其他所有情况下,呈现同意表单.
    default: return View(new AuthorizeViewModel
    {
        ApplicationName = await _applicationManager.GetLocalizedDisplayNameAsync(application),
        Scope = request.Scope
    });
}

临时授权

临时授权是在需要出于安全原因跟踪令牌链时由 OpenIddict 自动创建的,但开发者没有将明确的永久授权附加到用于登录操作的 ClaimsPrincipal 上。

这类授权通常在授权码流程中创建,用以链接与原始授权码相关的所有令牌,以便如果授权码被多次兑换(可能表明令牌泄露)时,它们能被自动撤销。同样,当在资源拥有者密码凭据授权请求过程中返回刷新令牌时,也会创建临时授权。

使用 OpenIddict.Quartz 集成时,临时授权会在短时间内(默认 14 天)自动从数据库中移除。与之不同的是,永久授权永远不会从数据库中移除。

在 API 级别启用授权条目验证

出于性能考虑,默认情况下,OpenIddict 3.0 在接收到 API 请求时不检查授权条目的状态:即使关联的授权被撤销,访问令牌也被视为有效。对于需要立即撤销授权的场景,可以配置 OpenIddict 验证处理器为每个 API 请求强制执行授权条目验证:

启用授权条目验证要求 OpenIddict 验证处理器直接访问存储授权的服务器数据库,这使得它更适合于与授权服务器位于同一应用程序中的 API。对于外部应用,请考虑使用 introspection( introspection endpoint 查询)而非本地验证。
在这两种情况下,由于额外的数据库请求和用于 introspection 的 HTTP 调用,预期会有更多的延迟。

services.AddOpenIddict()
    .AddValidation(options =>
    {
        options.EnableAuthorizationEntryValidation();
    });

禁用授权存储

虽然强烈不建议这样做,但在服务器选项中可以禁用授权存储:

services.AddOpenIddict()
    .AddServer(options =>
    {
        options.DisableAuthorizationStorage();
    });
在本文档中