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();
});