From bfa95d094a684be7bcc5b1869238c5eae3b28cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 12 Dec 2022 22:06:34 +0100 Subject: [PATCH] Unify the token entry properties restoration logic and fix an issue affecting device codes and ASP.NET Core Data Protection --- .../OpenIddictResources.resx | 19 +- ...ClientDataProtectionHandlers.Protection.cs | 10 +- .../OpenIddictClientBuilder.cs | 3 +- .../OpenIddictClientEvents.Protection.cs | 16 ++ .../OpenIddictClientExtensions.cs | 2 + .../OpenIddictClientHandlerFilters.cs | 32 +++ .../OpenIddictClientHandlers.Protection.cs | 111 +++++----- .../OpenIddictClientHandlers.cs | 3 + .../OpenIddictClientOptions.cs | 3 +- ...ServerDataProtectionHandlers.Protection.cs | 26 +-- .../OpenIddictServerEvents.Protection.cs | 16 ++ .../OpenIddictServerExtensions.cs | 2 + .../OpenIddictServerHandlerFilters.cs | 32 +++ .../OpenIddictServerHandlers.Protection.cs | 203 +++++++++--------- .../OpenIddictServerHandlers.cs | 23 +- ...dationDataProtectionHandlers.Protection.cs | 6 +- .../OpenIddictValidationEvents.Protection.cs | 10 + .../OpenIddictValidationExtensions.cs | 2 + .../OpenIddictValidationHandlerFilters.cs | 32 +++ ...OpenIddictValidationHandlers.Protection.cs | 84 ++++---- ...enIddictServerIntegrationTests.Exchange.cs | 48 ++++- .../OpenIddictServerIntegrationTests.cs | 9 + 22 files changed, 468 insertions(+), 224 deletions(-) diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 13050f5d..a4a1d0e6 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1936,6 +1936,12 @@ Consider registering a certificate using 'services.AddOpenIddict().AddClient().A The number of written bytes ({0}) doesn't match the expected value ({1}). + + The token identifier shouldn't be null or empty at this point. + + + The authorization identifier shouldn't be null or empty at this point. + An error occurred while validating the token '{Token}'. @@ -1976,14 +1982,21 @@ Consider registering a certificate using 'services.AddOpenIddict().AddClient().A The token entry for '{Type}' token '{Identifier}' was successfully created. - A new '{Type}' token was successfully created: {Payload}. + A new '{Type}' JSON Web Token was successfully created: {Payload}. The principal used to create the token contained the following claims: {Claims}. - The token entry for '{Type}' token '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'. + The token payload ({Payload}) was successfully attached to the token entry '{Identifier}' of type '{Type}'. + + + The reference identifier ({ReferenceId}) was successfully attached to the token entry '{Identifier}' of type '{Type}'. + + + 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}. - The reference token entry for device code '{Identifier}' was successfully updated'. + The token entry for device code '{Identifier}' was successfully updated with the new payload. The authorization request was successfully extracted: {Request}. diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs index 629661a6..9bb5b160 100644 --- a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs +++ b/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; diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs index bd7e4574..df2a9fb8 100644 --- a/src/OpenIddict.Client/OpenIddictClientBuilder.cs +++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs @@ -881,8 +881,7 @@ public sealed class OpenIddictClientBuilder /// /// 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. /// /// The instance. public OpenIddictClientBuilder DisableTokenStorage() diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs index b820d29c..9754af17 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs @@ -40,6 +40,12 @@ public static partial class OpenIddictClientEvents /// public bool CreateTokenEntry { get; set; } + /// + /// 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. + /// + public bool IsReferenceToken { get; set; } + /// /// 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 /// public string? TokenTypeHint { get; set; } = default!; + /// + /// Gets or sets a boolean indicating whether the validated token is a reference token. + /// + public bool IsReferenceToken { get; set; } + + /// + /// Gets or sets the authorization entry identifier associated with the token, if applicable. + /// + public string? AuthorizationId { get; set; } + /// /// Gets or sets the token entry identifier associated with the token, if applicable. /// diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs index d34db533..348e4e08 100644 --- a/src/OpenIddict.Client/OpenIddictClientExtensions.cs +++ b/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(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); @@ -54,6 +55,7 @@ public static class OpenIddictClientExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs index b627d36b..0a1825f6 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs @@ -27,6 +27,22 @@ public static class OpenIddictClientHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if no authorization identifier is resolved from the token. + /// + public sealed class RequireAuthorizationIdResolved : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!string.IsNullOrEmpty(context.AuthorizationId)); + } + } + /// /// Represents a filter that excludes the associated handlers if no backchannel access token is validated. /// @@ -305,6 +321,22 @@ public static class OpenIddictClientHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token. + /// + public sealed class RequireTokenIdResolved : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!string.IsNullOrEmpty(context.TokenId)); + } + } + /// /// Represents a filter that excludes the associated handlers if the token payload is not persisted in the database. /// diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs index d75924a9..35d9ef66 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs +++ b/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); /// /// Contains the logic responsible for resolving the validation parameters used to validate tokens. @@ -225,6 +225,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); } @@ -412,16 +413,16 @@ public static partial class OpenIddictClientHandlers } /// - /// 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. /// - public sealed class RestoreReferenceTokenProperties : IOpenIddictClientHandler + public sealed class RestoreTokenEntryProperties : IOpenIddictClientHandler { 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)); /// @@ -430,7 +431,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -442,20 +443,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)); } } @@ -470,7 +491,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) + .SetOrder(RestoreTokenEntryProperties.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -571,6 +592,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) @@ -585,28 +607,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, @@ -628,7 +636,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, @@ -637,13 +645,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)); } } @@ -853,16 +854,16 @@ public static partial class OpenIddictClientHandlers } /// - /// 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. /// - public sealed class ConvertReferenceToken : IOpenIddictClientHandler + public sealed class AttachTokenPayload : IOpenIddictClientHandler { 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)); /// @@ -872,7 +873,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(GenerateIdentityModelToken.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -900,14 +901,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); + } } } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 8dabf053..a846ad5a 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/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, diff --git a/src/OpenIddict.Client/OpenIddictClientOptions.cs b/src/OpenIddict.Client/OpenIddictClientOptions.cs index 9c4cd7c2..b73097f1 100644 --- a/src/OpenIddict.Client/OpenIddictClientOptions.cs +++ b/src/OpenIddict.Client/OpenIddictClientOptions.cs @@ -120,8 +120,7 @@ public sealed class OpenIddictClientOptions /// /// 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. /// public bool DisableTokenStorage { get; set; } diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs index 98ada848..9d85865f 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs +++ b/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; diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs index 04872f37..ca4b2ed2 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs @@ -46,6 +46,12 @@ public static partial class OpenIddictServerEvents /// public bool CreateTokenEntry { get; set; } + /// + /// 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. + /// + public bool IsReferenceToken { get; set; } + /// /// 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 /// public string? TokenTypeHint { get; set; } = default!; + /// + /// Gets or sets a boolean indicating whether the validated token is a reference token. + /// + public bool IsReferenceToken { get; set; } + + /// + /// Gets or sets the authorization entry identifier associated with the token, if applicable. + /// + public string? AuthorizationId { get; set; } + /// /// Gets or sets the token entry identifier associated with the token, if applicable. /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index 612389ae..c1c3f2a3 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -44,6 +44,7 @@ public static class OpenIddictServerExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); @@ -72,6 +73,7 @@ public static class OpenIddictServerExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs index 8b221a5c..f905a1c7 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -75,6 +75,22 @@ public static class OpenIddictServerHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if no authorization identifier is resolved from the token. + /// + public sealed class RequireAuthorizationIdResolved : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!string.IsNullOrEmpty(context.AuthorizationId)); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not an authorization request. /// @@ -507,6 +523,22 @@ public static class OpenIddictServerHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token. + /// + public sealed class RequireTokenIdResolved : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!string.IsNullOrEmpty(context.TokenId)); + } + } + /// /// Represents a filter that excludes the associated handlers if no token entry is created in the database. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index c3503c8c..b2dc69b6 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/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); /// @@ -231,6 +231,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); @@ -555,16 +556,16 @@ public static partial class OpenIddictServerHandlers } /// - /// 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. /// - public sealed class RestoreReferenceTokenProperties : IOpenIddictServerHandler + public sealed class RestoreTokenEntryProperties : IOpenIddictServerHandler { 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)); /// @@ -574,7 +575,7 @@ public static partial class OpenIddictServerHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -586,20 +587,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)); } } @@ -614,7 +649,7 @@ public static partial class OpenIddictServerHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) + .SetOrder(RestoreTokenEntryProperties.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -760,6 +795,7 @@ public static partial class OpenIddictServerHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) @@ -773,41 +809,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. @@ -818,7 +823,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, @@ -840,7 +845,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; } @@ -851,7 +856,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, @@ -864,7 +869,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, @@ -876,7 +881,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, @@ -900,13 +905,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 IsReusableAsync(object token) { // If the reuse leeway was set to null, return false to indicate @@ -963,6 +961,7 @@ public static partial class OpenIddictServerHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) @@ -976,17 +975,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, @@ -1144,7 +1138,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); @@ -1285,16 +1279,16 @@ public static partial class OpenIddictServerHandlers } /// - /// 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. /// - public sealed class ConvertReferenceToken : IOpenIddictServerHandler + public sealed class AttachTokenPayload : IOpenIddictServerHandler { 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)); /// @@ -1305,7 +1299,7 @@ public static partial class OpenIddictServerHandlers .AddFilter() .AddFilter() .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(GenerateIdentityModelToken.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1334,40 +1328,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); + } } } @@ -1387,7 +1388,7 @@ public static partial class OpenIddictServerHandlers // reference identifiers only works when the degraded mode is disabled. .AddFilter() .UseSingletonHandler() - .SetOrder(ConvertReferenceToken.Descriptor.Order + 1_000) + .SetOrder(AttachTokenPayload.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1402,7 +1403,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) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 9432d019..492b381c 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/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, diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs index e09ad676..9fbb13cf 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs +++ b/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)) diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs index 6d7f4edc..217504cf 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs @@ -49,6 +49,16 @@ public static partial class OpenIddictValidationEvents /// public string Token { get; set; } = default!; + /// + /// Gets or sets a boolean indicating whether the validated token is a reference token. + /// + public bool IsReferenceToken { get; set; } + + /// + /// Gets or sets the authorization entry identifier associated with the token, if applicable. + /// + public string? AuthorizationId { get; set; } + /// /// Gets or sets the token entry identifier associated with the token, if applicable. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index ab9ff035..0107debe 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -44,8 +44,10 @@ public static class OpenIddictValidationExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs index ea74b34f..6ab2580c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs @@ -59,6 +59,22 @@ public static class OpenIddictValidationHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if no authorization identifier is resolved from the token. + /// + public sealed class RequireAuthorizationIdResolved : IOpenIddictValidationHandlerFilter + { + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!string.IsNullOrEmpty(context.AuthorizationId)); + } + } + /// /// Represents a filter that excludes the associated handlers if local validation is not used. /// @@ -91,6 +107,22 @@ public static class OpenIddictValidationHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token. + /// + public sealed class RequireTokenIdResolved : IOpenIddictValidationHandlerFilter + { + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!string.IsNullOrEmpty(context.TokenId)); + } + } + /// /// Represents a filter that excludes the associated handlers if token validation was not enabled. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index dbc08b76..f9b0bab8 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/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, @@ -189,6 +189,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); } @@ -533,16 +534,16 @@ public static partial class OpenIddictValidationHandlers } /// - /// 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. /// - public sealed class RestoreReferenceTokenProperties : IOpenIddictValidationHandler + public sealed class RestoreTokenEntryProperties : IOpenIddictValidationHandler { 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)); /// @@ -552,7 +553,7 @@ public static partial class OpenIddictValidationHandlers = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -565,20 +566,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)); } } @@ -593,7 +614,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) + .SetOrder(RestoreTokenEntryProperties.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -761,6 +782,7 @@ public static partial class OpenIddictValidationHandlers = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(ValidateAudience.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -775,17 +797,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, @@ -794,13 +813,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)); } } @@ -825,6 +837,7 @@ public static partial class OpenIddictValidationHandlers = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() + .AddFilter() .UseScopedHandler() .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -839,17 +852,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, diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index 976d7ec4..0f34537c 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -530,6 +530,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.FindByReferenceIdAsync("g43LaWCUrz2RaLILz2L1bg1bOpMSv1hGrH12IIkB9H4", It.IsAny())) .ReturnsAsync(token); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.DeviceCode); + mock.Setup(manager => manager.HasTypeAsync(token, TokenTypeHints.DeviceCode, It.IsAny())) .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())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.AuthorizationCode); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(true); }); @@ -2448,6 +2456,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(true); @@ -2522,6 +2533,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(true); @@ -2596,6 +2610,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(true); @@ -2683,6 +2700,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) + .ReturnsAsync(TokenTypeHints.AuthorizationCode); + mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) .ReturnsAsync(true); @@ -2784,6 +2804,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) .ReturnsAsync(true); @@ -2877,6 +2900,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) .ReturnsAsync(true); @@ -2970,6 +2996,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) .ReturnsAsync(true); @@ -3062,6 +3091,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.AuthorizationCode); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(false); @@ -3149,6 +3181,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(false); @@ -3270,6 +3305,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.AuthorizationCode); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(false); @@ -3362,6 +3400,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(false); @@ -3881,6 +3922,11 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(flow is GrantTypes.AuthorizationCode ? + TokenTypeHints.AuthorizationCode : + TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) .ReturnsAsync(true); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index bb09a511..01e2cb4a 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -3057,6 +3057,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.AuthorizationCode); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) .ReturnsAsync(true); @@ -3132,6 +3135,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(false); @@ -3198,6 +3204,9 @@ public abstract partial class OpenIddictServerIntegrationTests mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) .ReturnsAsync(token); + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeHints.RefreshToken); + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) .ReturnsAsync(false);