From 2fd30447f03aaa1e19a902066ba810b53e880822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 25 Aug 2026 09:19:31 +0200 Subject: [PATCH] Enable session validation support in the server and validation stacks --- .../Program.cs | 7 +- .../Descriptors/OpenIddictTokenDescriptor.cs | 5 + .../Managers/IOpenIddictSessionManager.cs | 9 + .../Managers/IOpenIddictTokenManager.cs | 11 + .../OpenIddictResources.resx | 25 +- .../Stores/IOpenIddictTokenStore.cs | 20 + .../OpenIddictAuthorizationManager.cs | 6 + .../Managers/OpenIddictSessionManager.cs | 25 + .../Managers/OpenIddictTokenManager.cs | 28 + .../OpenIddictEntityFrameworkSessionStore.cs | 2 +- .../OpenIddictEntityFrameworkTokenStore.cs | 63 +- ...enIddictEntityFrameworkCoreSessionStore.cs | 2 +- ...OpenIddictEntityFrameworkCoreTokenStore.cs | 63 +- .../Stores/OpenIddictMongoDbTokenStore.cs | 18 + .../OpenIddictServerEvents.Protection.cs | 5 + .../OpenIddictServerExtensions.cs | 1 + .../OpenIddictServerHandlerFilters.cs | 14 + .../OpenIddictServerHandlers.Protection.cs | 47 + .../OpenIddictValidationBuilder.cs | 10 + .../OpenIddictValidationConfiguration.cs | 8 +- .../OpenIddictValidationEvents.Protection.cs | 5 + .../OpenIddictValidationExtensions.cs | 2 + .../OpenIddictValidationHandlerFilters.cs | 28 + ...OpenIddictValidationHandlers.Protection.cs | 48 +- .../OpenIddictValidationOptions.cs | 8 + ...enIddictServerIntegrationTests.Exchange.cs | 1155 ----------------- ...IddictServerIntegrationTests.Protection.cs | 316 +++++ .../OpenIddictServerIntegrationTests.cs | 17 + .../OpenIddictValidationIntegrationTests.cs | 17 + .../OpenIddictValidationConfigurationTests.cs | 46 +- 30 files changed, 827 insertions(+), 1184 deletions(-) diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs index 869066cc..62ba3e97 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Program.cs @@ -320,10 +320,11 @@ builder.Services.AddOpenIddict() // For applications that need immediate access token or authorization // revocation, the database entry of the received tokens and their // associated authorizations can be validated for each API call. - // Enabling these options may have a negative impact on performance. // - // options.EnableAuthorizationEntryValidation(); - // options.EnableTokenEntryValidation(); + // Note: enabling these options may have a negative impact on performance. + options.EnableAuthorizationEntryValidation() + .EnableSessionEntryValidation() + .EnableTokenEntryValidation(); }); builder.Services.AddTransient(); diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs index e080cc88..5872da11 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs @@ -60,6 +60,11 @@ public class OpenIddictTokenDescriptor /// public string? ReferenceId { get; set; } + /// + /// Gets or sets the identifier of the session associated with the token. + /// + public string? SessionId { get; set; } + /// /// Gets or sets the status of the token. /// diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs index 33eba6b0..d864b5f9 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictSessionManager.cs @@ -258,6 +258,15 @@ public interface IOpenIddictSessionManager /// ValueTask GetSubjectAsync(object session, CancellationToken cancellationToken = default); + /// + /// Determines whether a given session has the specified status. + /// + /// The session. + /// The expected status. + /// The that can be used to abort the operation. + /// if the session has the specified status, otherwise. + ValueTask HasStatusAsync(object session, string status, CancellationToken cancellationToken = default); + /// /// Executes the specified query and returns all the corresponding elements. /// diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index 7e3ffbbf..4f6bb35f 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -277,6 +277,17 @@ public interface IOpenIddictTokenManager /// ValueTask GetReferenceIdAsync(object token, CancellationToken cancellationToken = default); + /// + /// Retrieves the optional session identifier associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session identifier associated with the token. + /// + ValueTask GetSessionIdAsync(object token, CancellationToken cancellationToken = default); + /// /// Retrieves the status associated with a token. /// diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 00c09edd..0e5466e4 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -537,10 +537,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A The client secret cannot be null or empty when using introspection. Alternatively, one or multiple signing credentials can be registered and used as TLS client certificates or to produce client assertions if the authorization server supports it. - Authorization entry validation cannot be enabled when using introspection. - - - Token entry validation cannot be enabled when using introspection. + Authorization entry, session entry and token entry validation cannot be enabled when using introspection. A discovery client must be registered when using server discovery. @@ -557,7 +554,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A This may indicate that it was not properly registered in the dependency injection container. To register an event handler, use 'services.AddOpenIddict().AddValidation().AddEventHandler()'. - The core services must be registered when enabling token entry validation. + The core services must be registered when enabling authorization entry, session entry or token entry validation. To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'. @@ -566,10 +563,6 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag An unknown error occurred while introspecting the access token. - - The core services must be registered when enabling authorization entry validation. -To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'. - The URI cannot be null or empty. @@ -856,10 +849,7 @@ Reload the entity from the database and retry the operation. Make sure that the entity is not abstract and has a public parameterless constructor or create a custom store that overrides 'InstantiateAsync()' to use a custom factory. - The application matching the specified identifier cannot be found in the change tracker or in the database. - - - The authorization matching the specified identifier cannot be found in the change tracker or in the database. + The entity matching the specified identifier cannot be found in the change tracker or in the database. No Entity Framework Core context was configured to be used with OpenIddict. @@ -2430,6 +2420,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions The login identifier cannot be null or empty and must match the value used to represent the user session. + + The session associated with the token is no longer valid. + The '{0}' parameter shouldn't be null or empty at this point. @@ -2496,6 +2489,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions The length of the memory span ({0}) doesn't match the expected value ({1}). + + The session identifier shouldn't be null or empty at this point. + An error occurred while validating the token '{Token}'. @@ -3288,6 +3284,9 @@ This may indicate that the hashed entry is corrupted or malformed. A signing key of type '{Type}' was ignored because its ML-DSA public key couldn't be extracted. + + The session '{Identifier}' was no longer valid. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs index bcac5ea1..95c79333 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs @@ -232,6 +232,17 @@ public interface IOpenIddictTokenStore where TToken : class /// ValueTask GetReferenceIdAsync(TToken token, CancellationToken cancellationToken); + /// + /// Retrieves the optional session identifier associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session identifier associated with the token. + /// + ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken); + /// /// Retrieves the status associated with a token. /// @@ -359,6 +370,15 @@ public interface IOpenIddictTokenStore where TToken : class /// A that can be used to monitor the asynchronous operation. ValueTask SetAuthorizationIdAsync(TToken token, string? identifier, CancellationToken cancellationToken); + /// + /// Sets the session identifier associated with a token. + /// + /// The token. + /// The unique identifier associated with the token. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken); + /// /// Sets the creation date associated with a token. /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 67676ff6..1af5512c 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -133,6 +133,12 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori await Store.SetStatusAsync(authorization, Statuses.Valid, cancellationToken); } + // If no creation date was explicitly specified, set it to the current time. + if (await Store.GetCreationDateAsync(authorization, cancellationToken) is null) + { + await Store.SetCreationDateAsync(authorization, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken); + } + var results = await GetValidationResultsAsync(authorization, cancellationToken); if (results.Any(static result => result != ValidationResult.Success)) { diff --git a/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs b/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs index bbd910a8..22ccea72 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictSessionManager.cs @@ -132,6 +132,12 @@ public class OpenIddictSessionManager : IOpenIddictSessionManager wher await Store.SetStatusAsync(session, Statuses.Valid, cancellationToken); } + // If no creation date was explicitly specified, set it to the current time. + if (await Store.GetCreationDateAsync(session, cancellationToken) is null) + { + await Store.SetCreationDateAsync(session, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken); + } + var results = await GetValidationResultsAsync(session, cancellationToken); if (results.Any(static result => result != ValidationResult.Success)) { @@ -601,6 +607,21 @@ public class OpenIddictSessionManager : IOpenIddictSessionManager wher return Store.GetSubjectAsync(session, cancellationToken); } + /// + /// Determines whether a given session has the specified status. + /// + /// The session. + /// The expected status. + /// The that can be used to abort the operation. + /// if the session has the specified status, otherwise. + public virtual async ValueTask HasStatusAsync(TSession session, string status, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentException.ThrowIfNullOrEmpty(status); + + return string.Equals(await GetStatusAsync(session, cancellationToken), status, StringComparison.Ordinal); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -898,6 +919,10 @@ public class OpenIddictSessionManager : IOpenIddictSessionManager wher ValueTask IOpenIddictSessionManager.GetSubjectAsync(object session, CancellationToken cancellationToken) => GetSubjectAsync((TSession) session, cancellationToken); + /// + ValueTask IOpenIddictSessionManager.HasStatusAsync(object session, string status, CancellationToken cancellationToken) + => HasStatusAsync((TSession) session, status, cancellationToken); + /// IAsyncEnumerable IOpenIddictSessionManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) => ListAsync(count, offset, cancellationToken); diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index abeec969..1e181c51 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -134,6 +134,12 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok await Store.SetStatusAsync(token, Statuses.Valid, cancellationToken); } + // If no creation date was explicitly specified, set it to the current time. + if (await Store.GetCreationDateAsync(token, cancellationToken) is null) + { + await Store.SetCreationDateAsync(token, Options.CurrentValue.TimeProvider.GetUtcNow(), cancellationToken); + } + // If a reference identifier was set, obfuscate it. var identifier = await Store.GetReferenceIdAsync(token, cancellationToken); if (!string.IsNullOrEmpty(identifier)) @@ -632,6 +638,22 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok return Store.GetReferenceIdAsync(token, cancellationToken); } + /// + /// Retrieves the optional session identifier associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the session identifier associated with the token. + /// + public virtual ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(token); + + return Store.GetSessionIdAsync(token, cancellationToken); + } + /// /// Retrieves the status associated with a token. /// @@ -820,6 +842,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok await Store.SetPropertiesAsync(token, descriptor.Properties.ToImmutableDictionary(), cancellationToken); await Store.SetRedemptionDateAsync(token, descriptor.RedemptionDate, cancellationToken); await Store.SetReferenceIdAsync(token, descriptor.ReferenceId, cancellationToken); + await Store.SetSessionIdAsync(token, descriptor.SessionId, cancellationToken); await Store.SetStatusAsync(token, descriptor.Status, cancellationToken); await Store.SetSubjectAsync(token, descriptor.Subject, cancellationToken); await Store.SetTypeAsync(token, descriptor.Type, cancellationToken); @@ -848,6 +871,7 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok descriptor.Payload = await Store.GetPayloadAsync(token, cancellationToken); descriptor.RedemptionDate = await Store.GetRedemptionDateAsync(token, cancellationToken); descriptor.ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken); + descriptor.SessionId = await Store.GetSessionIdAsync(token, cancellationToken); descriptor.Status = await Store.GetStatusAsync(token, cancellationToken); descriptor.Subject = await Store.GetSubjectAsync(token, cancellationToken); descriptor.Type = await Store.GetTypeAsync(token, cancellationToken); @@ -1272,6 +1296,10 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok ValueTask IOpenIddictTokenManager.GetReferenceIdAsync(object token, CancellationToken cancellationToken) => GetReferenceIdAsync((TToken) token, cancellationToken); + /// + ValueTask IOpenIddictTokenManager.GetSessionIdAsync(object token, CancellationToken cancellationToken) + => GetSessionIdAsync((TToken) token, cancellationToken); + /// ValueTask IOpenIddictTokenManager.GetStatusAsync(object token, CancellationToken cancellationToken) => GetStatusAsync((TToken) token, cancellationToken); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs index e29494b6..7e08034d 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkSessionStore.cs @@ -644,7 +644,7 @@ public class OpenIddictEntityFrameworkSessionStore< session.Authorization = await context.Set().FindAsync( cancellationToken, ConvertIdentifierFromString(identifier)) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs index 12149931..0bf777e2 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs @@ -437,6 +437,33 @@ public class OpenIddictEntityFrameworkTokenStore< return new(token.ReferenceId); } + /// + public virtual async ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + // If the session is not attached to the token, try to load it manually. + if (token.Session is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Session); + if (reference.EntityEntry.State is EntityState.Detached) + { + return null; + } + + await reference.LoadAsync(cancellationToken); + } + + if (token.Session is null) + { + return null; + } + + return ConvertIdentifierToString(token.Session.Id); + } + /// public virtual ValueTask GetStatusAsync(TToken token, CancellationToken cancellationToken) { @@ -855,7 +882,7 @@ public class OpenIddictEntityFrameworkTokenStore< token.Authorization = await context.Set().FindAsync( cancellationToken, ConvertIdentifierFromString(identifier)) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -964,6 +991,40 @@ public class OpenIddictEntityFrameworkTokenStore< return ValueTask.CompletedTask; } + /// + public virtual async ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + if (!string.IsNullOrEmpty(identifier)) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + token.Session = await context.Set().FindAsync( + cancellationToken, ConvertIdentifierFromString(identifier)) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); + } + + else + { + // If the session is not attached to the token, try to load it manually. + if (token.Session is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Session); + if (reference.EntityEntry.State is EntityState.Detached) + { + return; + } + + await reference.LoadAsync(cancellationToken); + } + + token.Session = null; + } + } + /// public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs index 5e1b5617..db734be4 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreSessionStore.cs @@ -654,7 +654,7 @@ public class OpenIddictEntityFrameworkCoreSessionStore< session.Authorization = await context.Set() .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs index 78a302e5..c224d7be 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs @@ -418,6 +418,33 @@ public class OpenIddictEntityFrameworkCoreTokenStore< return new(token.ReferenceId); } + /// + public virtual async ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + // If the session is not attached to the token, try to load it manually. + if (token.Session is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Session); + if (reference.EntityEntry.State is EntityState.Detached) + { + return null; + } + + await reference.LoadAsync(cancellationToken); + } + + if (token.Session is null) + { + return null; + } + + return ConvertIdentifierToString(token.Session.Id); + } + /// public virtual ValueTask GetStatusAsync(TToken token, CancellationToken cancellationToken) { @@ -927,7 +954,7 @@ public class OpenIddictEntityFrameworkCoreTokenStore< token.Authorization = await context.Set() .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0251)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); } else @@ -950,6 +977,40 @@ public class OpenIddictEntityFrameworkCoreTokenStore< } } + /// + public virtual async ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + if (!string.IsNullOrEmpty(identifier)) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + token.Session = await context.Set() + .FindAsync([ConvertIdentifierFromString(identifier)], cancellationToken) + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0244)); + } + + else + { + // If the session is not attached to the token, try to load it manually. + if (token.Session is null) + { + var context = await Context.GetDbContextAsync(cancellationToken); + + var reference = context.Entry(token).Reference(static entry => entry.Session); + if (reference.EntityEntry.State is EntityState.Detached) + { + return; + } + + await reference.LoadAsync(cancellationToken); + } + + token.Session = null; + } + } + /// public virtual ValueTask SetCreationDateAsync(TToken token, DateTimeOffset? date, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs index 1b4e341b..a7bbcc14 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs @@ -319,6 +319,14 @@ public class OpenIddictMongoDbTokenStore< return new(token.ReferenceId); } + /// + public virtual ValueTask GetSessionIdAsync(TToken token, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + return new(token.SessionId != ObjectId.Empty ? token.SessionId.ToString() : null); + } + /// public virtual ValueTask GetStatusAsync(TToken token, CancellationToken cancellationToken) { @@ -622,6 +630,16 @@ public class OpenIddictMongoDbTokenStore< return ValueTask.CompletedTask; } + /// + public virtual ValueTask SetSessionIdAsync(TToken token, string? identifier, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(token); + + token.SessionId = !string.IsNullOrEmpty(identifier) ? ObjectId.Parse(identifier) : ObjectId.Empty; + + return ValueTask.CompletedTask; + } + /// public virtual ValueTask SetStatusAsync(TToken token, string? status, CancellationToken cancellationToken) { diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs index 32c71a11..c863b4f8 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs @@ -189,6 +189,11 @@ public static partial class OpenIddictServerEvents /// public string? AuthorizationId { get; set; } + /// + /// Gets or sets the session entry identifier associated with the token, if applicable. + /// + public string? SessionId { 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 baba1377..ecf195ea 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -82,6 +82,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 3a9b1500..d13b5333 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -655,6 +655,20 @@ public static class OpenIddictServerHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if no session identifier is resolved from the token. + /// + public sealed class RequireSessionIdResolved : IOpenIddictServerHandlerFilter + { + /// + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new(!string.IsNullOrEmpty(context.SessionId)); + } + } + /// /// Represents a filter that excludes the associated handlers if sliding refresh token expiration was disabled. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index c44f42ab..01b1aed9 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -44,6 +44,7 @@ public static partial class OpenIddictServerHandlers ValidateProofOfPossession.Descriptor, ValidateTokenEntry.Descriptor, ValidateAuthorizationEntry.Descriptor, + ValidateSessionEntry.Descriptor, /* * Token generation: @@ -832,6 +833,7 @@ public static partial class OpenIddictServerHandlers .SetCreationDate(await manager.GetCreationDateAsync(token, context.CancellationToken)) .SetExpirationDate(await manager.GetExpirationDateAsync(token, context.CancellationToken)) .SetAuthorizationId(context.AuthorizationId = await manager.GetAuthorizationIdAsync(token, context.CancellationToken)) + .SetSessionId(context.SessionId = await manager.GetSessionIdAsync(token, context.CancellationToken)) .SetTokenId(context.TokenId = await manager.GetIdAsync(token, context.CancellationToken)) .SetTokenType(await manager.GetTypeAsync(token, context.CancellationToken)); } @@ -1413,6 +1415,50 @@ public static partial class OpenIddictServerHandlers } } + /// + /// Contains the logic responsible for rejecting tokens whose + /// associated session entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when the degraded mode is enabled. + /// + public sealed class ValidateSessionEntry : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateAuthorizationEntry.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + public async ValueTask HandleAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + Debug.Assert(!string.IsNullOrEmpty(context.SessionId), SR.GetResourceString(SR.ID4022)); + + var manager = context.ServiceProvider.GetService() + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + var session = await manager.FindByIdAsync(context.SessionId, context.CancellationToken); + if (session is null || !await manager.HasStatusAsync(session, Statuses.Valid, context.CancellationToken)) + { + context.Logger.LogInformation(6297, SR.GetResourceString(SR.ID6297), context.SessionId); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2210), + uri: SR.FormatID8000(SR.ID2210)); + + return; + } + } + } + /// /// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens. /// @@ -1488,6 +1534,7 @@ public static partial class OpenIddictServerHandlers CreationDate = context.Principal.GetCreationDate(), ExpirationDate = context.Principal.GetExpirationDate(), Principal = context.Principal, + SessionId = context.Principal.GetSessionId(), Type = context.TokenType }; diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index bd8937fc..95611c29 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -592,6 +592,16 @@ public sealed class OpenIddictValidationBuilder public OpenIddictValidationBuilder EnableAuthorizationEntryValidation() => Configure(options => options.EnableAuthorizationEntryValidation = true); + /// + /// Enables session validation so that a database call is made for each API request + /// to ensure the session associated with the access token is still valid. + /// Note: enabling this option may have an impact on performance and + /// can only be used with an OpenIddict-based authorization server. + /// + /// The instance. + public OpenIddictValidationBuilder EnableSessionEntryValidation() + => Configure(options => options.EnableSessionEntryValidation = true); + /// /// Enables token validation so that a database call is made for each API request /// to ensure the token entry associated with the access token is still valid. diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index 4e3b326a..0df41d8e 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -172,15 +172,11 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions public string? AuthorizationId { get; set; } + /// + /// Gets or sets the session entry identifier associated with the token, if applicable. + /// + public string? SessionId { 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 67e1f76c..eafca58f 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -45,6 +45,8 @@ 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(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs index 4257ab06..07d2ac38 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs @@ -123,6 +123,34 @@ public static class OpenIddictValidationHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if session validation was not enabled. + /// + public sealed class RequireSessionEntryValidationEnabled : IOpenIddictValidationHandlerFilter + { + /// + public ValueTask IsActiveAsync(BaseContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new(context.Options.EnableSessionEntryValidation); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no session identifier is resolved from the token. + /// + public sealed class RequireSessionIdResolved : IOpenIddictValidationHandlerFilter + { + /// + public ValueTask IsActiveAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + return new(!string.IsNullOrEmpty(context.SessionId)); + } + } + /// /// Represents a filter that excludes the associated handlers if token audience validation was disabled. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index b5c15cc6..6414021d 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -42,6 +42,7 @@ public static partial class OpenIddictValidationHandlers ValidateProofOfPossession.Descriptor, ValidateTokenEntry.Descriptor, ValidateAuthorizationEntry.Descriptor, + ValidateSessionEntry.Descriptor, /* * Token generation: @@ -586,6 +587,7 @@ public static partial class OpenIddictValidationHandlers .SetCreationDate(await manager.GetCreationDateAsync(token, context.CancellationToken)) .SetExpirationDate(await manager.GetExpirationDateAsync(token, context.CancellationToken)) .SetAuthorizationId(context.AuthorizationId = await manager.GetAuthorizationIdAsync(token, context.CancellationToken)) + .SetSessionId(context.SessionId = await manager.GetSessionIdAsync(token, context.CancellationToken)) .SetTokenId(context.TokenId = await manager.GetIdAsync(token, context.CancellationToken)) .SetTokenType(await manager.GetTypeAsync(token, context.CancellationToken)); } @@ -955,7 +957,7 @@ public static partial class OpenIddictValidationHandlers Debug.Assert(!string.IsNullOrEmpty(context.AuthorizationId), SR.GetResourceString(SR.ID4018)); var manager = context.ServiceProvider.GetService() - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0142)); + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); var authorization = await manager.FindByIdAsync(context.AuthorizationId, context.CancellationToken); if (authorization is null || !await manager.HasStatusAsync(authorization, Statuses.Valid, context.CancellationToken)) @@ -972,6 +974,50 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible for rejecting tokens whose + /// associated session entry is no longer valid (e.g was revoked). + /// + public sealed class ValidateSessionEntry : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateAuthorizationEntry.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + ArgumentNullException.ThrowIfNull(context); + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + Debug.Assert(!string.IsNullOrEmpty(context.SessionId), SR.GetResourceString(SR.ID4022)); + + var manager = context.ServiceProvider.GetService() + ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); + + var session = await manager.FindByIdAsync(context.SessionId, context.CancellationToken); + if (session is null || !await manager.HasStatusAsync(session, Statuses.Valid, context.CancellationToken)) + { + context.Logger.LogInformation(6297, SR.GetResourceString(SR.ID6297), context.SessionId); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2210), + uri: SR.FormatID8000(SR.ID2210)); + + return; + } + } + } + /// /// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index a0a2aff8..1a6d3a25 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -101,6 +101,14 @@ public sealed class OpenIddictValidationOptions /// public bool EnableAuthorizationEntryValidation { get; set; } + /// + /// Gets or sets a boolean indicating whether a database call is made + /// to validate the session entry associated with the received tokens. + /// Note: enabling this option may have an impact on performance and + /// can only be used with an OpenIddict-based authorization server. + /// + public bool EnableSessionEntryValidation { get; set; } + /// /// Gets or sets a boolean indicating whether a database call is made /// to validate the token entry associated with the received tokens. diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index 327acbbc..c03e7524 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -4757,1161 +4757,6 @@ public abstract partial class OpenIddictServerIntegrationTests Mock.Get(manager).Verify(manager => manager.HasStatusAsync(tokens[1], Statuses.Valid, It.IsAny()), Times.Once()); } - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithCodeIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal([TokenTypeIdentifiers.Private.AuthorizationCode], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.Private.AuthorizationCode) - .SetPresenters("Fabrikam") - .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateApplicationManager(mock => - { - var application = new OpenIddictApplication(); - - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetSettingsAsync(application, It.IsAny())) - .ReturnsAsync(ImmutableDictionary.Create(StringComparer.Ordinal)); - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.Private.AuthorizationCode); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OpenIddictToken()); - })); - - options.Services.AddSingleton(manager); - - options.DisableAuthorizationStorage(); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithRefreshTokenIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - await using var server = await CreateServerAsync(options => - { - options.DisableRollingRefreshTokens(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OpenIddictToken()); - })); - - options.Services.AddSingleton(manager); - - options.DisableAuthorizationStorage(); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithSubjectTokenIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - await using var server = await CreateServerAsync(options => - { - options.DisableRollingRefreshTokens(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OpenIddictToken()); - })); - - options.Services.AddSingleton(manager); - - options.DisableAuthorizationStorage(); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_AuthorizationAssociatedWithActorTokenIsIgnoredWhenAuthorizationStorageIsDisabled() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(new OpenIddictAuthorization()); - }); - - await using var server = await CreateServerAsync(options => - { - options.DisableRollingRefreshTokens(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - if (string.Equals(context.Token, "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", StringComparison.Ordinal)) - { - Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.AccessToken) - .SetTokenId("E2894547-277E-4E09-A0C0-0A0631B54052") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - } - - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - ImmutableArray tokens = [new(), new()]; - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.FindByIdAsync("E2894547-277E-4E09-A0C0-0A0631B54052", It.IsAny())) - .ReturnsAsync(tokens[1]); - - mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("E2894547-277E-4E09-A0C0-0A0631B54052"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[1], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.AccessToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OpenIddictToken()); - })); - - options.Services.AddSingleton(manager); - - options.DisableAuthorizationStorage(); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", - ActorTokenType = TokenTypeIdentifiers.AccessToken, - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.NotNull(response.AccessToken); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeCannotBeFound() - { - // Arrange - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal([TokenTypeIdentifiers.Private.AuthorizationCode], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.Private.AuthorizationCode) - .SetPresenters("Fabrikam") - .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateApplicationManager(mock => - { - var application = new OpenIddictApplication(); - - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.Private.AuthorizationCode); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2020), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2020), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeIsInvalid() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal([TokenTypeIdentifiers.Private.AuthorizationCode], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.Private.AuthorizationCode) - .SetPresenters("Fabrikam") - .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateApplicationManager(mock => - { - var application = new OpenIddictApplication(); - - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.Private.AuthorizationCode); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ClientId = "Fabrikam", - Code = "SplxlOBeZQQYbYS6WxSbIA", - GrantType = GrantTypes.AuthorizationCode, - RedirectUri = "http://www.fabrikam.com/path" - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2020), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2020), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithRefreshTokenCannotBeFound() - { - // Arrange - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithRefreshTokenIsInvalid() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique"); - - return ValueTask.CompletedTask; - })); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.RefreshToken, - RefreshToken = "8xLOxBtZp8" - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithSubjectTokenCannotBeFound() - { - // Arrange - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithSubjectTokenIsInvalid() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - var token = new OpenIddictToken(); - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(token); - - mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", - ActorTokenType = TokenTypeIdentifiers.AccessToken, - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithActorTokenCannotBeFound() - { - // Arrange - var authorization = new OpenIddictAuthorization(); - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorization); - - mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.FindByIdAsync("224E3451-A3D4-48C3-A189-2D95B97C212A", It.IsAny())) - .ReturnsAsync(value: null); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - if (string.Equals(context.Token, "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", StringComparison.Ordinal)) - { - Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.AccessToken) - .SetTokenId("E2894547-277E-4E09-A0C0-0A0631B54052") - .SetAuthorizationId("224E3451-A3D4-48C3-A189-2D95B97C212A") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - } - - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - ImmutableArray tokens = [new(), new()]; - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.FindByIdAsync("E2894547-277E-4E09-A0C0-0A0631B54052", It.IsAny())) - .ReturnsAsync(tokens[1]); - - mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("E2894547-277E-4E09-A0C0-0A0631B54052"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[1], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.AccessToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("224E3451-A3D4-48C3-A189-2D95B97C212A"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", - ActorTokenType = TokenTypeIdentifiers.AccessToken, - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2023), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2023), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("224E3451-A3D4-48C3-A189-2D95B97C212A", It.IsAny()), Times.Once()); - } - - [Fact] - public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithActorTokenIsInvalid() - { - // Arrange - ImmutableArray authorizations = [new(), new()]; - - var manager = CreateAuthorizationManager(mock => - { - mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) - .ReturnsAsync(authorizations[0]); - - mock.Setup(manager => manager.HasStatusAsync(authorizations[0], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.FindByIdAsync("224E3451-A3D4-48C3-A189-2D95B97C212A", It.IsAny())) - .ReturnsAsync(authorizations[1]); - - mock.Setup(manager => manager.HasStatusAsync(authorizations[1], Statuses.Valid, It.IsAny())) - .ReturnsAsync(false); - }); - - await using var server = await CreateServerAsync(options => - { - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - if (string.Equals(context.Token, "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", StringComparison.Ordinal)) - { - Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.AccessToken) - .SetTokenId("E2894547-277E-4E09-A0C0-0A0631B54052") - .SetAuthorizationId("224E3451-A3D4-48C3-A189-2D95B97C212A") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - } - - Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeIdentifiers.RefreshToken) - .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetClaim(Claims.Subject, "Bob le Bricoleur"); - - return ValueTask.CompletedTask; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.Services.AddSingleton(CreateTokenManager(mock => - { - ImmutableArray tokens = [new(), new()]; - - mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) - .ReturnsAsync(tokens[0]); - - mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[0], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[0], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) - .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); - - mock.Setup(manager => manager.FindByIdAsync("E2894547-277E-4E09-A0C0-0A0631B54052", It.IsAny())) - .ReturnsAsync(tokens[1]); - - mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("E2894547-277E-4E09-A0C0-0A0631B54052"); - - mock.Setup(manager => manager.GetTypeAsync(tokens[1], It.IsAny())) - .ReturnsAsync(TokenTypeIdentifiers.AccessToken); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Redeemed, It.IsAny())) - .ReturnsAsync(false); - - mock.Setup(manager => manager.HasStatusAsync(tokens[1], Statuses.Valid, It.IsAny())) - .ReturnsAsync(true); - - mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[1], It.IsAny())) - .ReturnsAsync("224E3451-A3D4-48C3-A189-2D95B97C212A"); - })); - - options.Services.AddSingleton(manager); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/token", new OpenIddictRequest - { - ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", - ActorTokenType = TokenTypeIdentifiers.AccessToken, - GrantType = GrantTypes.TokenExchange, - SubjectToken = "8xLOxBtZp8", - SubjectTokenType = TokenTypeIdentifiers.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidGrant, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2023), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2023), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByIdAsync("224E3451-A3D4-48C3-A189-2D95B97C212A", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorizations[1], Statuses.Valid, It.IsAny()), Times.Once()); - } - [Fact] public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeCannotBeRedeemed() { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs index 9c5aa557..4fa9b184 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Protection.cs @@ -630,4 +630,320 @@ public abstract partial class OpenIddictServerIntegrationTests // Assert Assert.Equal(SR.FormatID0005(TokenTypeIdentifiers.Private.AuthorizationCode, TokenTypeIdentifiers.AccessToken), exception.Message); } + + [Fact] + public async Task ValidateToken_RequestIsRejectedWhenAuthorizationAssociatedWithTokenCannotBeFound() + { + // Arrange + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(value: null); + }); + + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.RefreshToken) + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + })); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.TokenExchange, + SubjectToken = "8xLOxBtZp8", + SubjectTokenType = TokenTypeIdentifiers.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateToken_RequestIsRejectedWhenAuthorizationAssociatedWithTokenIsInvalid() + { + // Arrange + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + + mock.Setup(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny())) + .ReturnsAsync(false); + }); + + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.RefreshToken) + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetAuthorizationIdAsync(token, It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + })); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", + ActorTokenType = TokenTypeIdentifiers.AccessToken, + GrantType = GrantTypes.TokenExchange, + SubjectToken = "8xLOxBtZp8", + SubjectTokenType = TokenTypeIdentifiers.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2022), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2022), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(authorization, Statuses.Valid, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateToken_RequestIsRejectedWhenSessionAssociatedWithTokenCannotBeFound() + { + // Arrange + var manager = CreateSessionManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny())) + .ReturnsAsync(value: null); + }); + + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.RefreshToken) + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSessionIdAsync(token, It.IsAny())) + .ReturnsAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5"); + })); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.TokenExchange, + SubjectToken = "8xLOxBtZp8", + SubjectTokenType = TokenTypeIdentifiers.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2210), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2210), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateToken_RequestIsRejectedWhenSessionAssociatedWithTokenIsInvalid() + { + // Arrange + var session = new OpenIddictSession(); + + var manager = CreateSessionManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny())) + .ReturnsAsync(session); + + mock.Setup(manager => manager.HasStatusAsync(session, Statuses.Valid, It.IsAny())) + .ReturnsAsync(false); + }); + + await using var server = await CreateServerAsync(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeIdentifiers.RefreshToken) + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetSessionId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("60FFF7EA-F98E-437B-937E-5073CC313103"); + + mock.Setup(manager => manager.GetTypeAsync(token, It.IsAny())) + .ReturnsAsync(TokenTypeIdentifiers.RefreshToken); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetSessionIdAsync(token, It.IsAny())) + .ReturnsAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5"); + })); + + options.Services.AddSingleton(manager); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ActorToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC", + ActorTokenType = TokenTypeIdentifiers.AccessToken, + GrantType = GrantTypes.TokenExchange, + SubjectToken = "8xLOxBtZp8", + SubjectTokenType = TokenTypeIdentifiers.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2210), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2210), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("DE7F0AF0-9595-4546-BE3D-F6BB43FB5FA5", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(session, Statuses.Valid, It.IsAny()), Times.Once()); + } } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 4ade1b67..fd3569a4 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -5203,12 +5203,14 @@ public abstract partial class OpenIddictServerIntegrationTests .SetDefaultAuthorizationEntity() .SetDefaultResourceEntity() .SetDefaultScopeEntity() + .SetDefaultSessionEntity() .SetDefaultTokenEntity(); options.Services.AddSingleton(CreateApplicationManager()) .AddSingleton(CreateAuthorizationManager()) .AddSingleton(CreateResourceManager()) .AddSingleton(CreateScopeManager()) + .AddSingleton(CreateSessionManager()) .AddSingleton(CreateTokenManager()); }) @@ -5343,6 +5345,20 @@ public abstract partial class OpenIddictServerIntegrationTests return manager.Object; } + protected OpenIddictSessionManager CreateSessionManager( + Action>>? configuration = null) + { + var manager = new Mock>( + Mock.Of>(), + OutputHelper.ToLogger>(), + Mock.Of>(), + Mock.Of>()); + + configuration?.Invoke(manager); + + return manager.Object; + } + protected OpenIddictTokenManager CreateTokenManager( Action>>? configuration = null) { @@ -5361,5 +5377,6 @@ public abstract partial class OpenIddictServerIntegrationTests public class OpenIddictAuthorization; public class OpenIddictResource; public class OpenIddictScope; + public class OpenIddictSession; public class OpenIddictToken; } diff --git a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs index 5ef0cafd..6c3ccad7 100644 --- a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs +++ b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs @@ -385,9 +385,11 @@ public abstract class OpenIddictValidationIntegrationTests .AddCore(options => { options.SetDefaultAuthorizationEntity() + .SetDefaultSessionEntity() .SetDefaultTokenEntity(); options.Services.AddSingleton(CreateAuthorizationManager()) + .AddSingleton(CreateSessionManager()) .AddSingleton(CreateTokenManager()); }) @@ -435,6 +437,20 @@ public abstract class OpenIddictValidationIntegrationTests return manager.Object; } + protected OpenIddictSessionManager CreateSessionManager( + Action>>? configuration = null) + { + var manager = new Mock>( + Mock.Of>(), + OutputHelper.ToLogger>(), + Mock.Of>(), + Mock.Of>()); + + configuration?.Invoke(manager); + + return manager.Object; + } + protected OpenIddictTokenManager CreateTokenManager( Action>>? configuration = null) { @@ -450,5 +466,6 @@ public abstract class OpenIddictValidationIntegrationTests } public class OpenIddictAuthorization; + public class OpenIddictSession; public class OpenIddictToken; } diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs index 22c80c82..c5e28b7f 100644 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs +++ b/test/OpenIddict.Validation.Tests/OpenIddictValidationConfigurationTests.cs @@ -308,7 +308,7 @@ public class OpenIddictValidationConfigurationTests } [Fact] - public void Validate_ReturnsAnErrorWhenAuthorizationOrTokenEntryValidationIsEnabledInIntrospectionMode() + public void Validate_ReturnsAnErrorWhenAuthorizationEntryValidationIsEnabledInIntrospectionMode() { // Arrange var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider()); @@ -320,6 +320,49 @@ public class OpenIddictValidationConfigurationTests options.ClientId = "client_id"; options.ClientSecret = "client_secret"; options.EnableAuthorizationEntryValidation = true; + options.ConfigurationManager = new StaticConfigurationManager(new OpenIddictConfiguration()); + + // Act + var result = configuration.Validate(name: null, options); + + // Assert + Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal); + } + + [Fact] + public void Validate_ReturnsAnErrorWhenSessionEntryValidationIsEnabledInIntrospectionMode() + { + // Arrange + var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider()); + var options = CreateBaseOptions(); + + options.ValidationType = OpenIddictValidationType.Introspection; + options.Issuer = new Uri("https://www.contoso.com/"); + options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration"); + options.ClientId = "client_id"; + options.ClientSecret = "client_secret"; + options.EnableSessionEntryValidation = true; + options.ConfigurationManager = new StaticConfigurationManager(new OpenIddictConfiguration()); + + // Act + var result = configuration.Validate(name: null, options); + + // Assert + Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal); + } + + [Fact] + public void Validate_ReturnsAnErrorWhenTokenEntryValidationIsEnabledInIntrospectionMode() + { + // Arrange + var configuration = new OpenIddictValidationConfiguration(new ServiceCollection().BuildServiceProvider()); + var options = CreateBaseOptions(); + + options.ValidationType = OpenIddictValidationType.Introspection; + options.Issuer = new Uri("https://www.contoso.com/"); + options.ConfigurationEndpoint = new Uri("https://www.contoso.com/.well-known/openid-configuration"); + options.ClientId = "client_id"; + options.ClientSecret = "client_secret"; options.EnableTokenEntryValidation = true; options.ConfigurationManager = new StaticConfigurationManager(new OpenIddictConfiguration()); @@ -328,7 +371,6 @@ public class OpenIddictValidationConfigurationTests // Assert Assert.Contains(SR.GetResourceString(SR.ID0133), result.Failures!, StringComparer.Ordinal); - Assert.Contains(SR.GetResourceString(SR.ID0134), result.Failures!, StringComparer.Ordinal); } [Fact]