Browse Source

Unify the token entry properties restoration logic and fix an issue affecting device codes and ASP.NET Core Data Protection

pull/1621/head
Kévin Chalet 3 years ago
parent
commit
21ccfc5e9b
  1. 19
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 10
      src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs
  3. 3
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  4. 16
      src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs
  5. 2
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  6. 32
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  7. 111
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  8. 3
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  9. 3
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  10. 26
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs
  11. 16
      src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs
  12. 2
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  13. 32
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  14. 203
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  15. 23
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  16. 6
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs
  17. 10
      src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
  18. 2
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  19. 32
      src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
  20. 84
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  21. 48
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  22. 9
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

19
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1939,6 +1939,12 @@ Consider registering a certificate using 'services.AddOpenIddict().AddClient().A
<data name="ID4016" xml:space="preserve">
<value>The number of written bytes ({0}) doesn't match the expected value ({1}).</value>
</data>
<data name="ID4017" xml:space="preserve">
<value>The token identifier shouldn't be null or empty at this point.</value>
</data>
<data name="ID4018" xml:space="preserve">
<value>The authorization identifier shouldn't be null or empty at this point.</value>
</data>
<data name="ID6000" xml:space="preserve">
<value>An error occurred while validating the token '{Token}'.</value>
</data>
@ -1979,14 +1985,21 @@ Consider registering a certificate using 'services.AddOpenIddict().AddClient().A
<value>The token entry for '{Type}' token '{Identifier}' was successfully created.</value>
</data>
<data name="ID6013" xml:space="preserve">
<value>A new '{Type}' token was successfully created: {Payload}.
<value>A new '{Type}' JSON Web Token was successfully created: {Payload}.
The principal used to create the token contained the following claims: {Claims}.</value>
</data>
<data name="ID6014" xml:space="preserve">
<value>The token entry for '{Type}' token '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'.</value>
<value>The token payload ({Payload}) was successfully attached to the token entry '{Identifier}' of type '{Type}'.</value>
</data>
<data name="ID6015" xml:space="preserve">
<value>The reference identifier ({ReferenceId}) was successfully attached to the token entry '{Identifier}' of type '{Type}'.</value>
</data>
<data name="ID6016" xml:space="preserve">
<value>A new '{Type}' ASP.NET Core Data Protection token was successfully created: {Payload}.
The principal used to create the token contained the following claims: {Claims}.</value>
</data>
<data name="ID6021" xml:space="preserve">
<value>The reference token entry for device code '{Identifier}' was successfully updated'.</value>
<value>The token entry for device code '{Identifier}' was successfully updated with the new payload.</value>
</data>
<data name="ID6030" xml:space="preserve">
<value>The authorization request was successfully extracted: {Request}.</value>

10
src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs

@ -104,11 +104,11 @@ public static partial class OpenIddictClientDataProtectionHandlers
//
// Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
(type, context.TokenId) switch
(type, context.IsReferenceToken) switch
{
(TokenTypeHints.StateToken, { Length: not 0 })
(TokenTypeHints.StateToken, true)
=> new[] { Handlers.Client, Formats.StateToken, Features.ReferenceTokens, Schemes.Server },
(TokenTypeHints.StateToken, null or { Length: 0 })
(TokenTypeHints.StateToken, false)
=> new[] { Handlers.Client, Formats.StateToken, Schemes.Server },
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
@ -220,7 +220,7 @@ public static partial class OpenIddictClientDataProtectionHandlers
//
// Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
(context.TokenType, context.PersistTokenPayload) switch
(context.TokenType, context.IsReferenceToken) switch
{
(TokenTypeHints.StateToken, true)
=> new[] { Handlers.Client, Formats.StateToken, Features.ReferenceTokens, Schemes.Server },
@ -237,7 +237,7 @@ public static partial class OpenIddictClientDataProtectionHandlers
context.Token = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType,
context.Logger.LogTrace(SR.GetResourceString(SR.ID6016), context.TokenType,
context.Token, context.Principal.Claims);
return default;

3
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -881,8 +881,7 @@ public sealed class OpenIddictClientBuilder
/// <summary>
/// Disables token storage, so that no database entry is created
/// for the tokens and codes returned by the OpenIddict client.
/// Using this option is generally NOT recommended as it prevents
/// the tokens from being revoked (if needed).
/// Using this option is generally NOT recommended.
/// </summary>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder DisableTokenStorage()

16
src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs

@ -40,6 +40,12 @@ public static partial class OpenIddictClientEvents
/// </summary>
public bool CreateTokenEntry { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a reference token should be used
/// and, if applicable, returned to the caller instead of the actual token payload.
/// </summary>
public bool IsReferenceToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the token payload
/// should be persisted alongside the token metadata in the database.
@ -129,6 +135,16 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? TokenTypeHint { get; set; } = default!;
/// <summary>
/// Gets or sets a boolean indicating whether the validated token is a reference token.
/// </summary>
public bool IsReferenceToken { get; set; }
/// <summary>
/// Gets or sets the authorization entry identifier associated with the token, if applicable.
/// </summary>
public string? AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the token entry identifier associated with the token, if applicable.
/// </summary>

2
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -37,6 +37,7 @@ public static class OpenIddictClientExtensions
// Register the built-in filters used by the default OpenIddict client event handlers.
builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>();
builder.Services.TryAddSingleton<RequireAuthorizationIdResolved>();
builder.Services.TryAddSingleton<RequireBackchannelAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireBackchannelIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireBackchannelIdentityTokenPrincipal>();
@ -54,6 +55,7 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireStateTokenPrincipal>();
builder.Services.TryAddSingleton<RequireStateTokenValidated>();
builder.Services.TryAddSingleton<RequireTokenEntryCreated>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>();
builder.Services.TryAddSingleton<RequireTokenRequest>();
builder.Services.TryAddSingleton<RequireTokenStorageEnabled>();

32
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -27,6 +27,22 @@ public static class OpenIddictClientHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no authorization identifier is resolved from the token.
/// </summary>
public sealed class RequireAuthorizationIdResolved : IOpenIddictClientHandlerFilter<ValidateTokenContext>
{
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.AuthorizationId));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no backchannel access token is validated.
/// </summary>
@ -305,6 +321,22 @@ public static class OpenIddictClientHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token.
/// </summary>
public sealed class RequireTokenIdResolved : IOpenIddictClientHandlerFilter<ValidateTokenContext>
{
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.TokenId));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the token payload is not persisted in the database.
/// </summary>

111
src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs

@ -28,7 +28,7 @@ public static partial class OpenIddictClientHandlers
ValidateReferenceTokenIdentifier.Descriptor,
ValidateIdentityModelToken.Descriptor,
MapInternalClaims.Descriptor,
RestoreReferenceTokenProperties.Descriptor,
RestoreTokenEntryProperties.Descriptor,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateTokenEntry.Descriptor,
@ -39,7 +39,7 @@ public static partial class OpenIddictClientHandlers
AttachSecurityCredentials.Descriptor,
CreateTokenEntry.Descriptor,
GenerateIdentityModelToken.Descriptor,
ConvertReferenceToken.Descriptor);
AttachTokenPayload.Descriptor);
/// <summary>
/// Contains the logic responsible for resolving the validation parameters used to validate tokens.
@ -245,6 +245,7 @@ public static partial class OpenIddictClientHandlers
// Replace the token parameter by the payload resolved from the token entry
// and store the identifier of the reference token so it can be later
// used to restore the properties associated with the token.
context.IsReferenceToken = true;
context.Token = payload;
context.TokenId = await _tokenManager.GetIdAsync(token);
}
@ -432,16 +433,16 @@ public static partial class OpenIddictClientHandlers
}
/// <summary>
/// Contains the logic responsible for restoring the properties associated with a reference token entry.
/// Contains the logic responsible for restoring the properties associated with a token entry.
/// Note: this handler is not used when token storage is disabled.
/// </summary>
public sealed class RestoreReferenceTokenProperties : IOpenIddictClientHandler<ValidateTokenContext>
public sealed class RestoreTokenEntryProperties : IOpenIddictClientHandler<ValidateTokenContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318));
public RestoreTokenEntryProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318));
public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager)
public RestoreTokenEntryProperties(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
@ -450,7 +451,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenStorageEnabled>()
.UseScopedHandler<RestoreReferenceTokenProperties>()
.UseScopedHandler<RestoreTokenEntryProperties>()
.SetOrder(MapInternalClaims.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -462,20 +463,40 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Principal is null || string.IsNullOrEmpty(context.TokenId))
if (context.Principal is null)
{
return;
}
var token = await _tokenManager.FindByIdAsync(context.TokenId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
// Extract the token identifier from the authentication principal.
//
// If no token identifier can be found, this indicates that the token
// has no backing database entry (e.g if token storage was disabled).
var identifier = context.Principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
// If the token entry cannot be found, return a generic error.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null)
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2019),
uri: SR.FormatID8000(SR.ID2019));
return;
}
// Restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
context.Principal
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(context.AuthorizationId = await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(context.TokenId = await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
}
}
@ -490,7 +511,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<ValidatePrincipal>()
.SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000)
.SetOrder(RestoreTokenEntryProperties.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -591,6 +612,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -605,28 +627,14 @@ public static partial class OpenIddictClientHandlers
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(!string.IsNullOrEmpty(context.TokenId), SR.GetResourceString(SR.ID4017));
var identifier = context.Principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
// If the token entry cannot be found, return a generic error.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null)
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2019),
uri: SR.FormatID8000(SR.ID2019));
return;
}
var token = await _tokenManager.FindByIdAsync(context.TokenId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
if (await _tokenManager.HasStatusAsync(token, Statuses.Redeemed))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), context.TokenId);
context.Reject(
error: Errors.InvalidToken,
@ -648,7 +656,7 @@ public static partial class OpenIddictClientHandlers
if (!await _tokenManager.HasStatusAsync(token, Statuses.Valid))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), context.TokenId);
context.Reject(
error: Errors.InvalidToken,
@ -657,13 +665,6 @@ public static partial class OpenIddictClientHandlers
return;
}
// Restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
}
}
@ -873,16 +874,16 @@ public static partial class OpenIddictClientHandlers
}
/// <summary>
/// Contains the logic responsible for converting the token to a reference token.
/// Contains the logic responsible for attaching the token payload to the token entry.
/// Note: this handler is not used when token storage is disabled.
/// </summary>
public sealed class ConvertReferenceToken : IOpenIddictClientHandler<GenerateTokenContext>
public sealed class AttachTokenPayload : IOpenIddictClientHandler<GenerateTokenContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public ConvertReferenceToken() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318));
public AttachTokenPayload() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318));
public ConvertReferenceToken(IOpenIddictTokenManager tokenManager)
public AttachTokenPayload(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
@ -892,7 +893,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireTokenPayloadPersisted>()
.UseScopedHandler<ConvertReferenceToken>()
.UseScopedHandler<AttachTokenPayload>()
.SetOrder(GenerateIdentityModelToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -920,14 +921,22 @@ public static partial class OpenIddictClientHandlers
// Attach the generated token to the token entry.
descriptor.Payload = context.Token;
descriptor.Principal = context.Principal;
descriptor.ReferenceId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
if (context.IsReferenceToken)
{
descriptor.ReferenceId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
}
await _tokenManager.UpdateAsync(token, descriptor);
// Replace the returned token by the reference identifier.
context.Token = descriptor.ReferenceId;
context.Logger.LogTrace(SR.GetResourceString(SR.ID6014), context.Token, identifier, context.TokenType);
context.Logger.LogTrace(SR.GetResourceString(SR.ID6014), context.TokenType, identifier, descriptor.ReferenceId);
// Replace the returned token by the reference identifier, if applicable.
if (context.IsReferenceToken)
{
context.Token = descriptor.ReferenceId;
context.Logger.LogTrace(SR.GetResourceString(SR.ID6015), descriptor.ReferenceId, identifier, context.TokenType);
}
}
}
}

3
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -2258,6 +2258,7 @@ public static partial class OpenIddictClientHandlers
var notification = new GenerateTokenContext(context.Transaction)
{
CreateTokenEntry = false,
IsReferenceToken = false,
PersistTokenPayload = false,
Principal = context.ClientAssertionTokenPrincipal!,
TokenFormat = TokenFormats.Jwt,
@ -4624,6 +4625,7 @@ public static partial class OpenIddictClientHandlers
var notification = new GenerateTokenContext(context.Transaction)
{
CreateTokenEntry = !context.Options.DisableTokenStorage,
IsReferenceToken = !context.Options.DisableTokenStorage,
PersistTokenPayload = !context.Options.DisableTokenStorage,
Principal = context.StateTokenPrincipal!,
TokenFormat = TokenFormats.Jwt,
@ -5215,6 +5217,7 @@ public static partial class OpenIddictClientHandlers
var notification = new GenerateTokenContext(context.Transaction)
{
CreateTokenEntry = !context.Options.DisableTokenStorage,
IsReferenceToken = !context.Options.DisableTokenStorage,
PersistTokenPayload = !context.Options.DisableTokenStorage,
Principal = context.StateTokenPrincipal!,
TokenFormat = TokenFormats.Jwt,

3
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -120,8 +120,7 @@ public sealed class OpenIddictClientOptions
/// <summary>
/// Gets or sets a boolean indicating whether token storage should be disabled.
/// When disabled, no database entry is created for the tokens created by the
/// OpenIddict client services. Using this option is generally NOT recommended
/// as it prevents the tokens from being revoked (if needed).
/// OpenIddict client services. Using this option is generally NOT recommended.
/// </summary>
public bool DisableTokenStorage { get; set; }

26
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs

@ -189,31 +189,31 @@ public static partial class OpenIddictServerDataProtectionHandlers
//
// Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
(type, context.TokenId) switch
(type, context.IsReferenceToken) switch
{
(TokenTypeHints.AccessToken, { Length: not 0 })
(TokenTypeHints.AccessToken, true)
=> new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server },
(TokenTypeHints.AccessToken, null or { Length: 0 })
(TokenTypeHints.AccessToken, false)
=> new[] { Handlers.Server, Formats.AccessToken, Schemes.Server },
(TokenTypeHints.AuthorizationCode, { Length: not 0 })
(TokenTypeHints.AuthorizationCode, true)
=> new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server },
(TokenTypeHints.AuthorizationCode, null or { Length: 0 })
(TokenTypeHints.AuthorizationCode, false)
=> new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server },
(TokenTypeHints.DeviceCode, { Length: not 0 })
(TokenTypeHints.DeviceCode, true)
=> new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server },
(TokenTypeHints.DeviceCode, null or { Length: 0 })
(TokenTypeHints.DeviceCode, false)
=> new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server },
(TokenTypeHints.RefreshToken, { Length: not 0 })
(TokenTypeHints.RefreshToken, true)
=> new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server },
(TokenTypeHints.RefreshToken, null or { Length: 0 })
(TokenTypeHints.RefreshToken, false)
=> new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server },
(TokenTypeHints.UserCode, { Length: not 0 })
(TokenTypeHints.UserCode, true)
=> new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server },
(TokenTypeHints.UserCode, null or { Length: 0 })
(TokenTypeHints.UserCode, false)
=> new[] { Handlers.Server, Formats.UserCode, Schemes.Server },
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
@ -337,7 +337,7 @@ public static partial class OpenIddictServerDataProtectionHandlers
//
// Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
(context.TokenType, context.PersistTokenPayload) switch
(context.TokenType, context.IsReferenceToken) switch
{
(TokenTypeHints.AccessToken, true)
=> new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server },
@ -374,7 +374,7 @@ public static partial class OpenIddictServerDataProtectionHandlers
context.Token = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType,
context.Logger.LogTrace(SR.GetResourceString(SR.ID6016), context.TokenType,
context.Token, context.Principal.Claims);
return default;

16
src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs

@ -46,6 +46,12 @@ public static partial class OpenIddictServerEvents
/// </summary>
public bool CreateTokenEntry { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a reference token should be used
/// and, if applicable, returned to the caller instead of the actual token payload.
/// </summary>
public bool IsReferenceToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the token payload
/// should be persisted alongside the token metadata in the database.
@ -135,6 +141,16 @@ public static partial class OpenIddictServerEvents
/// </summary>
public string? TokenTypeHint { get; set; } = default!;
/// <summary>
/// Gets or sets a boolean indicating whether the validated token is a reference token.
/// </summary>
public bool IsReferenceToken { get; set; }
/// <summary>
/// Gets or sets the authorization entry identifier associated with the token, if applicable.
/// </summary>
public string? AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the token entry identifier associated with the token, if applicable.
/// </summary>

2
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -44,6 +44,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeGenerated>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>();
builder.Services.TryAddSingleton<RequireAuthorizationIdResolved>();
builder.Services.TryAddSingleton<RequireAuthorizationStorageEnabled>();
builder.Services.TryAddSingleton<RequireAuthorizationRequest>();
builder.Services.TryAddSingleton<RequireClientIdParameter>();
@ -72,6 +73,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryCreated>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>();
builder.Services.TryAddSingleton<RequireTokenRequest>();

32
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -75,6 +75,22 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no authorization identifier is resolved from the token.
/// </summary>
public sealed class RequireAuthorizationIdResolved : IOpenIddictServerHandlerFilter<ValidateTokenContext>
{
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.AuthorizationId));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not an authorization request.
/// </summary>
@ -507,6 +523,22 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token.
/// </summary>
public sealed class RequireTokenIdResolved : IOpenIddictServerHandlerFilter<ValidateTokenContext>
{
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.TokenId));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no token entry is created in the database.
/// </summary>

203
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -29,7 +29,7 @@ public static partial class OpenIddictServerHandlers
ValidateIdentityModelToken.Descriptor,
NormalizeScopeClaims.Descriptor,
MapInternalClaims.Descriptor,
RestoreReferenceTokenProperties.Descriptor,
RestoreTokenEntryProperties.Descriptor,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateTokenEntry.Descriptor,
@ -41,7 +41,7 @@ public static partial class OpenIddictServerHandlers
AttachSecurityCredentials.Descriptor,
CreateTokenEntry.Descriptor,
GenerateIdentityModelToken.Descriptor,
ConvertReferenceToken.Descriptor,
AttachTokenPayload.Descriptor,
BeautifyToken.Descriptor);
/// <summary>
@ -241,6 +241,7 @@ public static partial class OpenIddictServerHandlers
// Replace the token parameter by the payload resolved from the token entry
// and store the identifier of the reference token so it can be later
// used to restore the properties associated with the token.
context.IsReferenceToken = true;
context.Token = payload;
context.TokenId = await _tokenManager.GetIdAsync(token);
@ -565,16 +566,16 @@ public static partial class OpenIddictServerHandlers
}
/// <summary>
/// Contains the logic responsible for restoring the properties associated with a reference token entry.
/// Contains the logic responsible for restoring the properties associated with a token entry.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public sealed class RestoreReferenceTokenProperties : IOpenIddictServerHandler<ValidateTokenContext>
public sealed class RestoreTokenEntryProperties : IOpenIddictServerHandler<ValidateTokenContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public RestoreTokenEntryProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager)
public RestoreTokenEntryProperties(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
@ -584,7 +585,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.UseScopedHandler<RestoreReferenceTokenProperties>()
.UseScopedHandler<RestoreTokenEntryProperties>()
.SetOrder(MapInternalClaims.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -596,20 +597,54 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Principal is null || string.IsNullOrEmpty(context.TokenId))
if (context.Principal is null)
{
return;
}
var token = await _tokenManager.FindByIdAsync(context.TokenId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
// Extract the token identifier from the authentication principal.
//
// If no token identifier can be found, this indicates that the token
// has no backing database entry (e.g if token storage was disabled).
var identifier = context.Principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
// If the token entry cannot be found, return a generic error.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null)
{
context.Reject(
error: Errors.InvalidToken,
description: context.Principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2001),
TokenTypeHints.DeviceCode => SR.GetResourceString(SR.ID2002),
TokenTypeHints.RefreshToken => SR.GetResourceString(SR.ID2003),
_ => SR.GetResourceString(SR.ID2004)
},
uri: context.Principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.FormatID8000(SR.ID2001),
TokenTypeHints.DeviceCode => SR.FormatID8000(SR.ID2002),
TokenTypeHints.RefreshToken => SR.FormatID8000(SR.ID2003),
_ => SR.FormatID8000(SR.ID2004)
});
return;
}
// Restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
context.Principal
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(context.AuthorizationId = await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(context.TokenId = await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
}
}
@ -624,7 +659,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<ValidatePrincipal>()
.SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000)
.SetOrder(RestoreTokenEntryProperties.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -770,6 +805,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
@ -783,41 +819,10 @@ public static partial class OpenIddictServerHandlers
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(!string.IsNullOrEmpty(context.TokenId), SR.GetResourceString(SR.ID4017));
// Extract the token identifier from the authentication principal.
// If no token identifier can be found, this indicates that the token
// has no backing database entry (e.g an access token or an identity token).
var identifier = context.Principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
// If the token entry cannot be found, return a generic error.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null)
{
context.Reject(
error: Errors.InvalidToken,
description: context.Principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2001),
TokenTypeHints.DeviceCode => SR.GetResourceString(SR.ID2002),
TokenTypeHints.RefreshToken => SR.GetResourceString(SR.ID2003),
_ => SR.GetResourceString(SR.ID2004)
},
uri: context.Principal.GetTokenType() switch
{
TokenTypeHints.AuthorizationCode => SR.FormatID8000(SR.ID2001),
TokenTypeHints.DeviceCode => SR.FormatID8000(SR.ID2002),
TokenTypeHints.RefreshToken => SR.FormatID8000(SR.ID2003),
_ => SR.FormatID8000(SR.ID2004)
});
return;
}
var token = await _tokenManager.FindByIdAsync(context.TokenId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
// If the token is already marked as redeemed, this may indicate that it was compromised.
// In this case, revoke the entire chain of tokens associated with the authorization.
@ -828,7 +833,7 @@ public static partial class OpenIddictServerHandlers
{
if (!context.Principal.HasTokenType(TokenTypeHints.RefreshToken) || !await IsReusableAsync(token))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), context.TokenId);
context.Reject(
error: Errors.InvalidToken,
@ -850,7 +855,7 @@ public static partial class OpenIddictServerHandlers
});
// Revoke all the token entries associated with the authorization.
await TryRevokeChainAsync(await _tokenManager.GetAuthorizationIdAsync(token));
await TryRevokeChainAsync(context.AuthorizationId);
return;
}
@ -861,7 +866,7 @@ public static partial class OpenIddictServerHandlers
// If the token is not marked as valid yet, return an authorization_pending error.
if (await _tokenManager.HasStatusAsync(token, Statuses.Inactive))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6003), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6003), context.TokenId);
context.Reject(
error: Errors.AuthorizationPending,
@ -874,7 +879,7 @@ public static partial class OpenIddictServerHandlers
// If the token is marked as rejected, return an access_denied error.
if (await _tokenManager.HasStatusAsync(token, Statuses.Rejected))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6004), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6004), context.TokenId);
context.Reject(
error: Errors.AccessDenied,
@ -886,7 +891,7 @@ public static partial class OpenIddictServerHandlers
if (!await _tokenManager.HasStatusAsync(token, Statuses.Valid))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), context.TokenId);
context.Reject(
error: Errors.InvalidToken,
@ -910,13 +915,6 @@ public static partial class OpenIddictServerHandlers
return;
}
// Restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
async ValueTask<bool> IsReusableAsync(object token)
{
// If the reuse leeway was set to null, return false to indicate
@ -973,6 +971,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireAuthorizationStorageEnabled>()
.AddFilter<RequireAuthorizationIdResolved>()
.UseScopedHandler<ValidateAuthorizationEntry>()
.SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
@ -986,17 +985,12 @@ public static partial class OpenIddictServerHandlers
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(!string.IsNullOrEmpty(context.AuthorizationId), SR.GetResourceString(SR.ID4018));
var identifier = context.Principal.GetAuthorizationId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
var authorization = await _authorizationManager.FindByIdAsync(identifier);
var authorization = await _authorizationManager.FindByIdAsync(context.AuthorizationId);
if (authorization is null || !await _authorizationManager.HasStatusAsync(authorization, Statuses.Valid))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), context.AuthorizationId);
context.Reject(
error: Errors.InvalidToken,
@ -1154,7 +1148,7 @@ public static partial class OpenIddictServerHandlers
var identifier = await _tokenManager.GetIdAsync(token);
// Attach the token identifier to the principal so that it can be stored in the token.
// Attach the token identifier to the principal so that it can be stored in the token payload.
context.Principal.SetTokenId(identifier);
context.Logger.LogTrace(SR.GetResourceString(SR.ID6012), context.TokenType, identifier);
@ -1295,16 +1289,16 @@ public static partial class OpenIddictServerHandlers
}
/// <summary>
/// Contains the logic responsible for converting the token to a reference token.
/// Contains the logic responsible for attaching the token payload to the token entry.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public sealed class ConvertReferenceToken : IOpenIddictServerHandler<GenerateTokenContext>
public sealed class AttachTokenPayload : IOpenIddictServerHandler<GenerateTokenContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public ConvertReferenceToken() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public AttachTokenPayload() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public ConvertReferenceToken(IOpenIddictTokenManager tokenManager)
public AttachTokenPayload(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
@ -1315,7 +1309,7 @@ public static partial class OpenIddictServerHandlers
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireTokenPayloadPersisted>()
.UseScopedHandler<ConvertReferenceToken>()
.UseScopedHandler<AttachTokenPayload>()
.SetOrder(GenerateIdentityModelToken.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -1344,40 +1338,47 @@ public static partial class OpenIddictServerHandlers
descriptor.Payload = context.Token;
descriptor.Principal = context.Principal;
if (context.TokenType is TokenTypeHints.UserCode)
if (context.IsReferenceToken)
{
do
if (context.TokenType is TokenTypeHints.UserCode)
{
// Note: unlike other reference tokens, user codes are meant to be used by humans,
// who may have to enter it in a web form. To ensure they remain easy enough to type
// even by users with non-Latin keyboards, user codes generated by OpenIddict are
// only compound of 12 digits, generated using a crypto-secure random number generator.
// In this case, the resulting user code is estimated to have at most ~40 bits of entropy.
do
{
// Note: unlike other reference tokens, user codes are meant to be used by humans,
// who may have to enter it in a web form. To ensure they remain easy enough to type
// even by users with non-Latin keyboards, user codes generated by OpenIddict are
// only compound of 12 digits, generated using a crypto-secure random number generator.
// In this case, the resulting user code is estimated to have at most ~40 bits of entropy.
static string CreateRandomNumericCode(int length) => OpenIddictHelpers.CreateRandomString(
charset: stackalloc[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
length: length);
static string CreateRandomNumericCode(int length) => OpenIddictHelpers.CreateRandomString(
charset: stackalloc[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
length: length);
descriptor.ReferenceId = CreateRandomNumericCode(length: 12);
}
descriptor.ReferenceId = CreateRandomNumericCode(length: 12);
}
// User codes are relatively short. To help reduce the risks of collisions with
// existing entries, a database check is performed here before updating the entry.
while (await _tokenManager.FindByReferenceIdAsync(descriptor.ReferenceId) is not null);
}
// User codes are relatively short. To help reduce the risks of collisions with
// existing entries, a database check is performed here before updating the entry.
while (await _tokenManager.FindByReferenceIdAsync(descriptor.ReferenceId) is not null);
}
else
{
// For other tokens, generate a base64url-encoded 256-bit random identifier.
descriptor.ReferenceId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
else
{
// For other tokens, generate a base64url-encoded 256-bit random identifier.
descriptor.ReferenceId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
}
}
await _tokenManager.UpdateAsync(token, descriptor);
// Replace the returned token by the reference identifier.
context.Token = descriptor.ReferenceId;
context.Logger.LogTrace(SR.GetResourceString(SR.ID6014), context.Token, identifier, context.TokenType);
context.Logger.LogTrace(SR.GetResourceString(SR.ID6014), context.TokenType, identifier, descriptor.ReferenceId);
// Replace the returned token by the reference identifier, if applicable.
if (context.IsReferenceToken)
{
context.Token = descriptor.ReferenceId;
context.Logger.LogTrace(SR.GetResourceString(SR.ID6015), descriptor.ReferenceId, identifier, context.TokenType);
}
}
}
@ -1397,7 +1398,7 @@ public static partial class OpenIddictServerHandlers
// reference identifiers only works when the degraded mode is disabled.
.AddFilter<RequireDegradedModeDisabled>()
.UseSingletonHandler<BeautifyToken>()
.SetOrder(ConvertReferenceToken.Descriptor.Order + 1_000)
.SetOrder(AttachTokenPayload.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -1412,7 +1413,7 @@ public static partial class OpenIddictServerHandlers
// To make user codes easier to read and type by humans, a dash is automatically
// appended before each new block of 4 integers. These dashes are expected to be
// stripped from the user codes when receiving them at the verification endpoint.
if (context.TokenType is TokenTypeHints.UserCode)
if (context.IsReferenceToken && context.TokenType is TokenTypeHints.UserCode)
{
var builder = new StringBuilder(context.Token);
if (builder.Length % 4 != 0)

23
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -2412,6 +2412,7 @@ public static partial class OpenIddictServerHandlers
CreateTokenEntry = !context.Options.DisableTokenStorage,
// Access tokens can be converted to reference tokens if the
// corresponding option was enabled in the server options.
IsReferenceToken = context.Options.UseReferenceAccessTokens,
PersistTokenPayload = context.Options.UseReferenceAccessTokens,
Principal = context.AccessTokenPrincipal!,
TokenFormat = TokenFormats.Jwt,
@ -2478,6 +2479,7 @@ public static partial class OpenIddictServerHandlers
{
ClientId = context.ClientId,
CreateTokenEntry = !context.Options.DisableTokenStorage,
IsReferenceToken = !context.Options.DisableTokenStorage,
PersistTokenPayload = !context.Options.DisableTokenStorage,
Principal = context.AuthorizationCodePrincipal!,
TokenFormat = TokenFormats.Jwt,
@ -2543,8 +2545,16 @@ public static partial class OpenIddictServerHandlers
var notification = new GenerateTokenContext(context.Transaction)
{
ClientId = context.ClientId,
CreateTokenEntry = !context.Options.DisableTokenStorage,
// Device codes can be converted to reference tokens if they are not generated
// Don't create a new entry if the device code is generated as part
// of a device code swap made by the user code verification endpoint.
CreateTokenEntry = context.EndpointType switch
{
OpenIddictServerEndpointType.Verification => false,
_ => !context.Options.DisableTokenStorage
},
IsReferenceToken = !context.Options.DisableTokenStorage,
// Device codes are not persisted using the generic logic if they are generated
// as part of a device code swap made by the user code verification endpoint.
PersistTokenPayload = context.EndpointType switch
{
@ -2554,7 +2564,7 @@ public static partial class OpenIddictServerHandlers
},
Principal = context.DeviceCodePrincipal!,
TokenFormat = TokenFormats.Jwt,
TokenType = TokenTypeHints.DeviceCode
TokenType = TokenTypeHints.DeviceCode,
};
await _dispatcher.DispatchAsync(notification);
@ -2619,6 +2629,7 @@ public static partial class OpenIddictServerHandlers
CreateTokenEntry = !context.Options.DisableTokenStorage,
// Refresh tokens can be converted to reference tokens if the
// corresponding option was enabled in the server options.
IsReferenceToken = context.Options.UseReferenceRefreshTokens,
PersistTokenPayload = context.Options.UseReferenceRefreshTokens,
Principal = context.RefreshTokenPrincipal!,
TokenFormat = TokenFormats.Jwt,
@ -2738,7 +2749,7 @@ public static partial class OpenIddictServerHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0020));
}
// Extract the token identifier from the authentication principal.
// Extract the device code identifier from the user code principal.
var identifier = context.Principal.GetClaim(Claims.Private.DeviceCodeId);
if (string.IsNullOrEmpty(identifier))
{
@ -2890,6 +2901,7 @@ public static partial class OpenIddictServerHandlers
ClientId = context.ClientId,
CreateTokenEntry = !context.Options.DisableTokenStorage,
PersistTokenPayload = !context.Options.DisableTokenStorage,
IsReferenceToken = !context.Options.DisableTokenStorage,
Principal = context.UserCodePrincipal!,
TokenFormat = TokenFormats.Jwt,
TokenType = TokenTypeHints.UserCode
@ -2955,7 +2967,8 @@ public static partial class OpenIddictServerHandlers
{
ClientId = context.ClientId,
CreateTokenEntry = !context.Options.DisableTokenStorage,
// Identity tokens cannot never be reference tokens.
// Identity tokens cannot be reference tokens.
IsReferenceToken = false,
PersistTokenPayload = false,
Principal = context.IdentityTokenPrincipal!,
TokenFormat = TokenFormats.Jwt,

6
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs

@ -98,11 +98,11 @@ public static partial class OpenIddictValidationDataProtectionHandlers
//
// Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
(type, context.TokenId) switch
(type, context.IsReferenceToken) switch
{
(TokenTypeHints.AccessToken, { Length: not 0 })
(TokenTypeHints.AccessToken, true)
=> new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server },
(TokenTypeHints.AccessToken, null or { Length: 0 })
(TokenTypeHints.AccessToken, false)
=> new[] { Handlers.Server, Formats.AccessToken, Schemes.Server },
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))

10
src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs

@ -49,6 +49,16 @@ public static partial class OpenIddictValidationEvents
/// </summary>
public string Token { get; set; } = default!;
/// <summary>
/// Gets or sets a boolean indicating whether the validated token is a reference token.
/// </summary>
public bool IsReferenceToken { get; set; }
/// <summary>
/// Gets or sets the authorization entry identifier associated with the token, if applicable.
/// </summary>
public string? AuthorizationId { get; set; }
/// <summary>
/// Gets or sets the token entry identifier associated with the token, if applicable.
/// </summary>

2
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -44,8 +44,10 @@ public static class OpenIddictValidationExtensions
builder.Services.TryAddSingleton<RequireAccessTokenExtracted>();
builder.Services.TryAddSingleton<RequireAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireAuthorizationEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireAuthorizationIdResolved>();
builder.Services.TryAddSingleton<RequireLocalValidation>();
builder.Services.TryAddSingleton<RequireTokenEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireIntrospectionValidation>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.

32
src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs

@ -59,6 +59,22 @@ public static class OpenIddictValidationHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no authorization identifier is resolved from the token.
/// </summary>
public sealed class RequireAuthorizationIdResolved : IOpenIddictValidationHandlerFilter<ValidateTokenContext>
{
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.AuthorizationId));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if local validation is not used.
/// </summary>
@ -91,6 +107,22 @@ public static class OpenIddictValidationHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token.
/// </summary>
public sealed class RequireTokenIdResolved : IOpenIddictValidationHandlerFilter<ValidateTokenContext>
{
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.TokenId));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if token validation was not enabled.
/// </summary>

84
src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs

@ -28,7 +28,7 @@ public static partial class OpenIddictValidationHandlers
IntrospectToken.Descriptor,
NormalizeScopeClaims.Descriptor,
MapInternalClaims.Descriptor,
RestoreReferenceTokenProperties.Descriptor,
RestoreTokenEntryProperties.Descriptor,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateAudience.Descriptor,
@ -199,6 +199,7 @@ public static partial class OpenIddictValidationHandlers
// Replace the token parameter by the payload resolved from the token entry
// and store the identifier of the reference token so it can be later
// used to restore the properties associated with the token.
context.IsReferenceToken = true;
context.Token = payload;
context.TokenId = await _tokenManager.GetIdAsync(token);
}
@ -543,16 +544,16 @@ public static partial class OpenIddictValidationHandlers
}
/// <summary>
/// Contains the logic responsible for restoring the properties associated with a reference token entry.
/// Contains the logic responsible for restoring the properties associated with a token entry.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public sealed class RestoreReferenceTokenProperties : IOpenIddictValidationHandler<ValidateTokenContext>
public sealed class RestoreTokenEntryProperties : IOpenIddictValidationHandler<ValidateTokenContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139));
public RestoreTokenEntryProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139));
public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager)
public RestoreTokenEntryProperties(IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
/// <summary>
@ -562,7 +563,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireTokenEntryValidationEnabled>()
.UseScopedHandler<RestoreReferenceTokenProperties>()
.UseScopedHandler<RestoreTokenEntryProperties>()
.SetOrder(MapInternalClaims.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -575,20 +576,40 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Principal is null || string.IsNullOrEmpty(context.TokenId))
if (context.Principal is null)
{
return;
}
var token = await _tokenManager.FindByIdAsync(context.TokenId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
// Extract the token identifier from the authentication principal.
//
// If no token identifier can be found, this indicates that the token
// has no backing database entry (e.g if token storage was disabled).
var identifier = context.Principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
// If the token entry cannot be found, return a generic error.
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null)
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2019),
uri: SR.FormatID8000(SR.ID2019));
return;
}
// Restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
context.Principal
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(context.AuthorizationId = await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(context.TokenId = await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
}
}
@ -603,7 +624,7 @@ public static partial class OpenIddictValidationHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<ValidatePrincipal>()
.SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000)
.SetOrder(RestoreTokenEntryProperties.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -771,6 +792,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireTokenEntryValidationEnabled>()
.AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateAudience.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
@ -785,17 +807,14 @@ public static partial class OpenIddictValidationHandlers
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(!string.IsNullOrEmpty(context.TokenId), SR.GetResourceString(SR.ID4017));
var identifier = context.Principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
var token = await _tokenManager.FindByIdAsync(context.TokenId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
var token = await _tokenManager.FindByIdAsync(identifier);
if (token is null || !await _tokenManager.HasStatusAsync(token, Statuses.Valid))
if (!await _tokenManager.HasStatusAsync(token, Statuses.Valid))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), context.TokenId);
context.Reject(
error: Errors.InvalidToken,
@ -804,13 +823,6 @@ public static partial class OpenIddictValidationHandlers
return;
}
// Restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetTokenId(await _tokenManager.GetIdAsync(token))
.SetTokenType(await _tokenManager.GetTypeAsync(token));
}
}
@ -835,6 +847,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireAuthorizationEntryValidationEnabled>()
.AddFilter<RequireAuthorizationIdResolved>()
.UseScopedHandler<ValidateAuthorizationEntry>()
.SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
@ -849,17 +862,12 @@ public static partial class OpenIddictValidationHandlers
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(!string.IsNullOrEmpty(context.AuthorizationId), SR.GetResourceString(SR.ID4018));
var identifier = context.Principal.GetAuthorizationId();
if (string.IsNullOrEmpty(identifier))
{
return;
}
var authorization = await _authorizationManager.FindByIdAsync(identifier);
var authorization = await _authorizationManager.FindByIdAsync(context.AuthorizationId);
if (authorization is null || !await _authorizationManager.HasStatusAsync(authorization, Statuses.Valid))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), identifier);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), context.AuthorizationId);
context.Reject(
error: Errors.InvalidToken,

48
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -530,6 +530,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.FindByReferenceIdAsync("g43LaWCUrz2RaLILz2L1bg1bOpMSv1hGrH12IIkB9H4", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.DeviceCode);
mock.Setup(manager => manager.HasTypeAsync(token, TokenTypeHints.DeviceCode, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -559,7 +562,9 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(new[] { TokenTypeHints.DeviceCode }, context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Bricoleur");
.SetClaim(Claims.Subject, "Bob le Bricoleur")
.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103")
.SetTokenType(TokenTypeHints.DeviceCode);
return default;
});
@ -2365,6 +2370,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.AuthorizationCode);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
});
@ -2448,6 +2456,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -2522,6 +2533,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -2596,6 +2610,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -2683,6 +2700,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.AuthorizationCode);
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -2784,6 +2804,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -2877,6 +2900,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -2970,6 +2996,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -3062,6 +3091,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.AuthorizationCode);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
@ -3149,6 +3181,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
@ -3270,6 +3305,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.AuthorizationCode);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
@ -3362,6 +3400,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
@ -3881,6 +3922,11 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(flow is GrantTypes.AuthorizationCode ?
TokenTypeHints.AuthorizationCode :
TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);

9
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -3057,6 +3057,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.AuthorizationCode);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
@ -3132,6 +3135,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.GetIdAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103");
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
@ -3198,6 +3204,9 @@ public abstract partial class OpenIddictServerIntegrationTests
mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(TokenTypeHints.RefreshToken);
mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);

Loading…
Cancel
Save