From 831a5b988a8181c1e655485a4a3a3d221a035220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 8 Jan 2020 04:27:10 +0100 Subject: [PATCH] Port the sign-in integration tests --- .../OpenIddictServerEvents.cs | 11 - ...OpenIddictServerHandlers.Authentication.cs | 2 +- .../OpenIddictServerHandlers.Exchange.cs | 2 +- .../OpenIddictServerHandlers.cs | 53 +- .../OpenIddictServerProvider.cs | 4 - .../Primitives/OpenIddictExtensionsTests.cs | 12 +- ...enIddictServerIntegrationTests.Exchange.cs | 93 +- .../OpenIddictServerIntegrationTests.cs | 2425 ++++++++++++++++- 8 files changed, 2521 insertions(+), 81 deletions(-) diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index dc194131..99133dae 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -247,17 +247,6 @@ namespace OpenIddict.Server /// Gets the client identifier, or null if the client application is unknown. /// public string ClientId => Request.ClientId; - - /// - /// Gets a boolean indicating whether the - /// method was called. - /// - public bool IsHandled { get; private set; } - - /// - /// Marks the authentication process as handled by the application code. - /// - public void HandleAuthentication() => IsHandled = true; } /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index cc7c785b..33daed0c 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -301,7 +301,7 @@ namespace OpenIddict.Server else if (@event.IsRejected) { context.Reject( - error: @event.Error ?? Errors.InvalidGrant, + error: @event.Error ?? Errors.InvalidRequest, description: @event.ErrorDescription, uri: @event.ErrorUri); return; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 3c086556..e9040dad 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -299,7 +299,7 @@ namespace OpenIddict.Server else if (@event.IsRejected) { context.Reject( - error: @event.Error ?? Errors.InvalidGrant, + error: @event.Error ?? Errors.InvalidRequest, description: @event.ErrorDescription, uri: @event.ErrorUri); return; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 0f46d385..82c39261 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -55,7 +55,7 @@ namespace OpenIddict.Server /* * Sign-in processing: */ - ValidateSigninDemand.Descriptor, + ValidateSignInDemand.Descriptor, RestoreInternalClaims.Descriptor, AttachDefaultScopes.Descriptor, AttachDefaultPresenters.Descriptor, @@ -156,7 +156,6 @@ namespace OpenIddict.Server .AppendLine("to validate a token for an invalid grant type (e.g password).") .ToString()); - default: throw new InvalidOperationException(new StringBuilder() .AppendLine("An identity cannot be extracted from this request.") .Append("This generally indicates that the OpenIddict server stack was asked ") @@ -1221,14 +1220,14 @@ namespace OpenIddict.Server /// Contains the logic responsible of ensuring that the sign-in demand /// is compatible with the type of the endpoint that handled the request. /// - public class ValidateSigninDemand : IOpenIddictServerHandler + public class ValidateSignInDemand : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); @@ -1303,7 +1302,7 @@ namespace OpenIddict.Server if (string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject))) { throw new InvalidOperationException(new StringBuilder() - .AppendLine("The specified principal was rejected because the mandatory subject claim was missing.") + .Append("The specified principal was rejected because the mandatory subject claim was missing.") .ToString()); } @@ -1322,7 +1321,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateSigninDemand.Descriptor.Order + 1_000) + .SetOrder(ValidateSignInDemand.Descriptor.Order + 1_000) .Build(); /// @@ -1754,6 +1753,13 @@ namespace OpenIddict.Server return true; } + // Never exclude the presenters and scope private claims. + if (string.Equals(claim.Type, Claims.Private.Presenters, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.Scopes, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + // Never include the public or internal token identifiers to ensure the identifiers // that are automatically inherited from the parent token are not reused for the new token. if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) || @@ -1807,22 +1813,13 @@ namespace OpenIddict.Server // Set the public audiences collection using the private resource claims stored in the principal. principal.SetAudiences(context.Principal.GetResources()); - // Set the authorized party using the first presenters (typically the client identifier), if available. - principal.SetClaim(Claims.AuthorizedParty, context.Principal.GetPresenters().FirstOrDefault()); - - // Set the public scope claim using the private scope claims from the principal. - // Note: scopes are deliberately formatted as a single space-separated - // string to respect the usual representation of the standard scope claim. - // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. - principal.SetClaim(Claims.Scope, string.Join(" ", context.Principal.GetScopes())); - // When receiving a grant_type=refresh_token request, determine whether the client application // requests a limited set of scopes and immediately replace the scopes collection if necessary. if (context.EndpointType == OpenIddictServerEndpointType.Token && context.Request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(context.Request.Scope)) { var scopes = context.Request.GetScopes(); - principal.SetClaim(Claims.Scope, string.Join(" ", scopes.Intersect(context.Principal.GetScopes()))); + principal.SetScopes(scopes.Intersect(context.Principal.GetScopes())); context.Logger.LogDebug("The access token scopes will be limited to the scopes " + "requested by the client application: {Scopes}.", scopes); @@ -2695,6 +2692,26 @@ namespace OpenIddict.Server return default; } + // Copy the principal and exclude the presenters/scopes private claims, + // that are manually mapped to public standard azp/scope JWT claims. + var principal = context.AccessTokenPrincipal.Clone(claim => claim.Type switch + { + Claims.Private.Presenters => false, + Claims.Private.Scopes => false, + Claims.Private.TokenId => false, + + _ => true + }); + + // Set the authorized party using the first presenters (typically the client identifier), if available. + principal.SetClaim(Claims.AuthorizedParty, context.AccessTokenPrincipal.GetPresenters().FirstOrDefault()); + + // Set the public scope claim using the private scope claims from the principal. + // Note: scopes are deliberately formatted as a single space-separated + // string to respect the usual representation of the standard scope claim. + // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. + principal.SetClaim(Claims.Scope, string.Join(" ", context.AccessTokenPrincipal.GetScopes())); + var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor { AdditionalHeaderClaims = new Dictionary(StringComparer.Ordinal) @@ -2704,7 +2721,7 @@ namespace OpenIddict.Server Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.AccessTokenPrincipal.Identity + Subject = (ClaimsIdentity) principal.Identity }); var credentials = context.Options.EncryptionCredentials.FirstOrDefault( @@ -2723,7 +2740,7 @@ namespace OpenIddict.Server context.Logger.LogTrace("The access token '{Identifier}' was successfully created: {Payload}. " + "The principal used to create the token contained the following claims: {Claims}.", context.AccessTokenPrincipal.GetClaim(Claims.JwtId), - context.Response.AccessToken, context.AccessTokenPrincipal.Claims); + context.Response.AccessToken, principal.Claims); return default; } diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.cs b/src/OpenIddict.Server/OpenIddictServerProvider.cs index dcd8b388..7911b25d 100644 --- a/src/OpenIddict.Server/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/OpenIddictServerProvider.cs @@ -67,10 +67,6 @@ namespace OpenIddict.Server _logger.LogDebug("The request was rejected in user code."); return; - case BaseValidatingTicketContext notification when notification.IsHandled: - _logger.LogDebug("Authentication was handled in user code."); - return; - default: continue; } } diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs index 04c61260..1590ed22 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs @@ -1362,7 +1362,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = (ClaimsIdentity) null; // Act and assert - var exception = Assert.Throws(() => identity.Clone(c => true)); + var exception = Assert.Throws(() => identity.Clone(claim => true)); Assert.Equal("identity", exception.ParamName); } @@ -1375,7 +1375,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives identity.AddClaim("type", "value"); // Act - var copy = identity.Clone(c => true); + var copy = identity.Clone(claim => true); // Assert Assert.Equal("value", copy.GetClaim("type")); @@ -1389,7 +1389,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var principal = (ClaimsPrincipal) null; // Act and assert - var exception = Assert.Throws(() => principal.Clone(c => true)); + var exception = Assert.Throws(() => principal.Clone(claim => true)); Assert.Equal("principal", exception.ParamName); } @@ -1404,7 +1404,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var principal = new ClaimsPrincipal(identity); // Act - var copy = principal.Clone(c => true); + var copy = principal.Clone(claim => true); // Assert Assert.Equal("Bob le Bricoleur", copy.GetClaim(Claims.Name)); @@ -1419,7 +1419,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives identity.AddClaim("type", "value"); // Act - var copy = identity.Clone(c => true); + var copy = identity.Clone(claim => true); copy.AddClaim("clone_type", "value"); // Assert @@ -1437,7 +1437,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var principal = new ClaimsPrincipal(identity); // Act - var copy = principal.Clone(c => true); + var copy = principal.Clone(claim => true); copy.SetClaim("clone_claim", "value"); // Assert diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index b70ca52e..45dc2836 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -300,7 +300,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetExpirationDate(DateTimeOffset.UtcNow - TimeSpan.FromDays(1)); + .SetExpirationDate(DateTimeOffset.UtcNow - TimeSpan.FromDays(1)) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -338,7 +339,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetExpirationDate(DateTimeOffset.UtcNow - TimeSpan.FromDays(1)); + .SetExpirationDate(DateTimeOffset.UtcNow - TimeSpan.FromDays(1)) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -375,7 +377,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetPresenters(Enumerable.Empty()); + .SetPresenters(Enumerable.Empty()) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -414,7 +417,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetPresenters("Contoso"); + .SetPresenters("Contoso") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -452,7 +456,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetPresenters("Contoso"); + .SetPresenters("Contoso") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -491,6 +496,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetClaim(Claims.Private.RedirectUri, "http://www.fabrikam.com/callback"); return default; @@ -531,6 +537,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetClaim(Claims.Private.RedirectUri, "http://www.fabrikam.com/callback"); return default; @@ -572,6 +579,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetClaim(Claims.Private.CodeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") .SetClaim(Claims.Private.CodeChallengeMethod, CodeChallengeMethods.Sha256); @@ -613,6 +621,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetClaim(Claims.Private.CodeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") .SetClaim(Claims.Private.CodeChallengeMethod, null); @@ -655,6 +664,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetClaim(Claims.Private.CodeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") .SetClaim(Claims.Private.CodeChallengeMethod, "custom_code_challenge_method"); @@ -699,6 +709,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetClaim(Claims.Private.CodeChallenge, challenge) .SetClaim(Claims.Private.CodeChallengeMethod, method); @@ -741,8 +752,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Magnifique") .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetClaim(Claims.Private.CodeChallenge, challenge) .SetClaim(Claims.Private.CodeChallengeMethod, method); @@ -783,7 +794,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") - .SetScopes(Enumerable.Empty()); + .SetScopes(Enumerable.Empty()) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -823,7 +835,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") - .SetScopes("profile", "email"); + .SetScopes("profile", "email") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -862,7 +875,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetScopes(Enumerable.Empty()); + .SetScopes(Enumerable.Empty()) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -900,7 +914,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetScopes("profile", "email"); + .SetScopes("profile", "email") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -1640,7 +1655,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -1691,7 +1707,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -1735,7 +1752,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -1794,7 +1812,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -1848,7 +1867,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -1915,7 +1935,8 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -1965,7 +1986,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2047,7 +2069,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2138,7 +2161,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2225,7 +2249,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2287,7 +2312,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2360,7 +2386,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2408,7 +2435,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2492,7 +2520,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2560,7 +2589,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2645,7 +2675,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetPresenters("Fabrikam") .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2725,7 +2756,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2796,7 +2828,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; }); @@ -2878,9 +2911,9 @@ namespace OpenIddict.Server.FunctionalTests builder.UseInlineHandler(context => { context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetPresenters("Fabrikam") - .SetInternalTokenId("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC"); + .SetInternalTokenId("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); if (context.Request.IsAuthorizationCodeGrantType()) { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 33feb6c8..f8e4ec23 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -5,8 +5,11 @@ */ using System; +using System.Collections.Immutable; +using System.Linq; using System.Security.Claims; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -423,6 +426,2414 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal("Bob le Magnifique", (string) response[Claims.Subject]); } + [Fact] + public async Task ProcessSignIn_UnknownEndpointCausesAnException() + { + // Arrange + var client = CreateClient(); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.PostAsync("/signin", new OpenIddictRequest()); + }); + + Assert.Equal("An OpenID Connect response cannot be returned from this endpoint.", exception.Message); + } + + [Fact] + public async Task ProcessSignIn_InvalidEndpointCausesAnException() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetConfigurationEndpointUris("/signin"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/signin"); + }); + + Assert.Equal("An OpenID Connect response cannot be returned from this endpoint.", exception.Message); + } + + [Fact] + public async Task ProcessSignIn_NullIdentityCausesAnException() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(); + + return default; + })); + }); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + }); + + Assert.Equal(new StringBuilder() + .AppendLine("The specified principal doesn't contain any claims-based identity.") + .Append("Make sure that both 'ClaimsPrincipal.Identity' is not null.") + .ToString(), exception.Message); + } + + [Fact] + public async Task ProcessSignIn_NullAuthenticationTypeCausesAnException() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity()); + + return default; + })); + }); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + }); + + Assert.Equal(new StringBuilder() + .AppendLine("The specified principal doesn't contain a valid/authenticated identity.") + .Append("Make sure that 'ClaimsPrincipal.Identity.AuthenticationType' is not null ") + .Append("and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'true'.") + .ToString(), exception.Message); + } + + [Fact] + public async Task ProcessSignIn_MissingSubjectCausesAnException() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")); + + return default; + })); + }); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Code + }); + }); + + Assert.Equal("The specified principal was rejected because the mandatory subject claim was missing.", exception.Message); + } + + [Fact] + public async Task ProcessSignIn_ScopeDefaultsToOpenId() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + Assert.Equal(new[] { Scopes.OpenId }, context.Principal.GetScopes()); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = Scopes.OpenId + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ProcessSignIn_ResourcesAreInferredFromAudiences() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + Assert.Equal(new[] { "http://www.fabrikam.com/" }, context.Principal.GetResources()); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetAudiences("http://www.fabrikam.com/") + .SetScopes(Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.NotNull(response.AccessToken); + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_AllowsOverridingDefaultTokensSelection() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + context.IncludeAccessToken = false; + context.IncludeAuthorizationCode = true; + context.IncludeIdentityToken = true; + context.IncludeRefreshToken = true; + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Null(response.AccessToken); + Assert.NotNull(response.Code); + Assert.NotNull(response.IdToken); + Assert.NotNull(response.RefreshToken); + } + + [Theory] + [InlineData("code")] + [InlineData("code id_token")] + [InlineData("code id_token token")] + [InlineData("code token")] + public async Task ProcessSignIn_AnAuthorizationCodeIsReturnedForCodeAndHybridFlowRequests(string type) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeAuthorizationCode); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = type, + Scope = Scopes.OpenId + }); + + // Assert + Assert.NotNull(response.Code); + } + + [Fact] + public async Task ProcessSignIn_ScopesCanBeOverridenForRefreshTokenRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.RegisterScopes(Scopes.Profile); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.Profile, Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal(new[] { Scopes.Profile }, context.AccessTokenPrincipal.GetScopes()); + + return default; + }); + + builder.SetOrder(PrepareAccessTokenPrincipal.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8", + Scope = Scopes.Profile + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ProcessSignIn_ScopesAreReturnedWhenTheyDifferFromRequestedScopes() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.RegisterScopes(Scopes.Phone, Scopes.Profile); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.Profile) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = "openid phone profile" + }); + + // Assert + Assert.Equal(Scopes.Profile, response.Scope); + } + + [Theory] + [InlineData("code id_token token")] + [InlineData("code token")] + [InlineData("id_token token")] + [InlineData("token")] + public async Task ProcessSignIn_AnAccessTokenIsReturnedForImplicitAndHybridFlowRequests(string type) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeAccessToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = type, + Scope = Scopes.OpenId + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ProcessSignIn_AnAccessTokenIsReturnedForCodeGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeAccessToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ProcessSignIn_AnAccessTokenIsReturnedForRefreshTokenGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeAccessToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ProcessSignIn_AnAccessTokenIsReturnedForPasswordGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeAccessToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ProcessSignIn_AnAccessTokenIsReturnedForClientCredentialsGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeAccessToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.ClientCredentials, + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ProcessSignIn_AnAccessTokenIsReturnedForCustomGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeAccessToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = "urn:ietf:params:oauth:grant-type:custom_grant" + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ProcessSignIn_ExpiresInIsReturnedWhenExpirationDateIsKnown() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.NotNull(response.ExpiresIn); + } + + [Fact] + public async Task ProcessSignIn_NoRefreshTokenIsReturnedWhenOfflineAccessScopeIsNotGranted() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.False(context.IncludeRefreshToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Null(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_ARefreshTokenIsReturnedForCodeGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetScopes(Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeRefreshToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_NoRefreshTokenIsReturnedForRefreshTokenGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.False(context.IncludeRefreshToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_NoRefreshTokenIsReturnedWhenSlidingExpirationIsDisabled() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.DisableSlidingExpiration(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.False(context.IncludeRefreshToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_ARefreshTokenIsReturnedForPasswordGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeRefreshToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_ARefreshTokenIsReturnedForClientCredentialsGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeRefreshToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.ClientCredentials, + }); + + // Assert + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_ARefreshTokenIsReturnedForCustomGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeRefreshToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = "urn:ietf:params:oauth:grant-type:custom_grant" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_NoIdentityTokenIsReturnedWhenOfflineAccessScopeIsNotGranted() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.False(context.IncludeIdentityToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Null(response.IdToken); + } + + [Theory] + [InlineData("code id_token")] + [InlineData("code id_token token")] + [InlineData("id_token")] + [InlineData("id_token token")] + public async Task ProcessSignIn_AnIdentityTokenIsReturnedForImplicitAndHybridFlowRequests(string type) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeIdentityToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = type, + Scope = Scopes.OpenId + }); + + // Assert + Assert.NotNull(response.IdToken); + } + + [Fact] + public async Task ProcessSignIn_AnIdentityTokenIsReturnedForCodeGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetScopes(Scopes.OpenId) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeIdentityToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.NotNull(response.IdToken); + } + + [Fact] + public async Task ProcessSignIn_AnIdentityTokenIsReturnedForRefreshTokenGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeIdentityToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.IdToken); + } + + [Fact] + public async Task ProcessSignIn_AnIdentityTokenIsReturnedForPasswordGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeIdentityToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = Scopes.OpenId + }); + + // Assert + Assert.NotNull(response.IdToken); + } + + [Fact] + public async Task ProcessSignIn_AnIdentityTokenIsReturnedForClientCredentialsGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeIdentityToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.ClientCredentials, + Scope = Scopes.OpenId + }); + + // Assert + Assert.NotNull(response.IdToken); + } + + [Fact] + public async Task ProcessSignIn_AnIdentityTokenIsReturnedForCustomGrantRequests() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.True(context.IncludeIdentityToken); + + return default; + }); + + builder.SetOrder(EvaluateReturnedTokens.Descriptor.Order + 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = "urn:ietf:params:oauth:grant-type:custom_grant", + Scope = Scopes.OpenId + }); + + // Assert + Assert.NotNull(response.IdToken); + } + + [Theory] + [InlineData("custom_error", null, null)] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, null, "custom_uri")] + [InlineData(null, null, null)] + public async Task ProcessSignIn_AllowsRejectingAuthorizationRequest(string error, string description, string uri) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Reject(error, description, uri); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Code, + Scope = Scopes.OpenId + }); + + // Assert + Assert.Equal(error ?? Errors.InvalidRequest, response.Error); + Assert.Equal(description, response.ErrorDescription); + Assert.Equal(uri, response.ErrorUri); + } + + [Theory] + [InlineData("custom_error", null, null)] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, null, "custom_uri")] + [InlineData(null, null, null)] + public async Task ProcessSignIn_AllowsRejectingTokenRequest(string error, string description, string uri) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Reject(error, description, uri); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(error ?? Errors.InvalidRequest, response.Error); + Assert.Equal(description, response.ErrorDescription); + Assert.Equal(uri, response.ErrorUri); + } + + [Fact] + public async Task ProcessSignIn_AllowsHandlingResponse() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Transaction.SetProperty("custom_response", new + { + name = "Bob le Bricoleur" + }); + + context.HandleRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal("Bob le Bricoleur", (string) response["name"]); + } + + [Fact] + public async Task ProcessSignIn_PrivateClaimsAreAutomaticallyRestored() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.UseRollingTokens(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + Assert.Equal(new[] { Scopes.OpenId, Scopes.OfflineAccess }, context.Principal.GetScopes()); + Assert.Equal("value", context.Principal.GetClaim(Claims.Prefixes.Private + "_private_claim")); + + return default; + })); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Bricoleur") + .SetClaim(Claims.Prefixes.Private + "_private_claim", "value"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.IdToken); + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_RefreshTokenIsIssuedForAuthorizationCodeRequestsWhenRollingTokensAreEnabled() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.UseRollingTokens(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // 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.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_RefreshTokenIsAlwaysIssuedWhenRollingTokensAreEnabled() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.UseRollingTokens(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_RefreshTokenIsNotIssuedWhenRollingTokensAreDisabled() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + } + + [Fact] + public async Task ProcessSignIn_AuthorizationCodeIsAutomaticallyRedeemed() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + 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.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateApplicationManager(mock => + { + var application = new OpenIddictApplication(); + + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Public); + })); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_ReturnsErrorResponseWhenRedeemingAuthorizationCodeFails() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + 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.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateApplicationManager(mock => + { + var application = new OpenIddictApplication(); + + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Public); + })); + + options.Services.AddSingleton(manager); + }); + + // 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("The specified authorization code is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_RefreshTokenIsAutomaticallyRedeemedWhenRollingTokensAreEnabled() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + 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.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny())) + .ReturnsAsync(true); + }); + + var client = CreateClient(options => + { + options.UseRollingTokens(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_ReturnsErrorResponseWhenRedeemingRefreshTokenFails() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + 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.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.TryRedeemAsync(token, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.UseRollingTokens(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_RefreshTokenIsNotRedeemedWhenRollingTokensAreDisabled() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.TryRedeemAsync(token, It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSignIn_PreviousTokensAreAutomaticallyRevokedWhenRollingTokensAreEnabled() + { + // Arrange + var tokens = new[] + { + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken() + }; + + var manager = CreateTokenManager(mock => + { + 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.GetIdAsync(tokens[1], It.IsAny())) + .ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103"); + + mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny())) + .ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8"); + + mock.Setup(manager => manager.GetAuthorizationIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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.TryRedeemAsync(tokens[0], It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .Returns(tokens.ToAsyncEnumerable()); + }); + + var client = CreateClient(options => + { + options.UseRollingTokens(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateAuthorizationManager(mock => + { + var authorization = new OpenIddictAuthorization(); + + 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); + })); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.RefreshToken); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny()), Times.Never()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_PreviousTokensAreNotRevokedWhenRollingTokensAreDisabled() + { + // Arrange + var tokens = new[] + { + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken() + }; + + var manager = CreateTokenManager(mock => + { + 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.GetIdAsync(tokens[1], It.IsAny())) + .ReturnsAsync("481FCAC6-06BC-43EE-92DB-37A78AA09B595073CC313103"); + + mock.Setup(manager => manager.GetIdAsync(tokens[2], It.IsAny())) + .ReturnsAsync("3BEA7A94-5ADA-49AF-9F41-8AB6156E31A8"); + + 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.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .Returns(tokens.ToAsyncEnumerable()); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(CreateAuthorizationManager(mock => + { + var authorization = new OpenIddictAuthorization(); + + 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); + })); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.AccessToken); + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny()), Times.Never()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[1], It.IsAny()), Times.Never()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[2], It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSignIn_ExtendsLifetimeWhenRollingTokensAreDisabledAndSlidingExpirationEnabled() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + 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.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token, + It.IsAny(), It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_DoesNotExtendLifetimeWhenSlidingExpirationIsDisabled() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + }); + + var client = CreateClient(options => + { + options.DisableSlidingExpiration(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token, + It.IsAny(), It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSignIn_DoesNotUpdateExpirationDateWhenAlreadyNull() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + 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.GetExpirationDateAsync(token, It.IsAny())) + .ReturnsAsync(value: null); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token, null, It.IsAny()), Times.Never()); + } + + [Fact] + public async Task ProcessSignIn_SetsExpirationDateToNullWhenLifetimeIsNull() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + 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.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetExpirationDateAsync(token, It.IsAny())) + .ReturnsAsync(DateTimeOffset.Now + TimeSpan.FromDays(1)); + }); + + var client = CreateClient(options => + { + options.SetRefreshTokenLifetime(lifetime: null); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Null(response.RefreshToken); + + Mock.Get(manager).Verify(manager => manager.TryExtendAsync(token, null, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_IgnoresErrorWhenExtendingLifetimeOfExistingTokenFailed() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(mock => + { + 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.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.TryExtendAsync(token, It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + }); + + // 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.TryExtendAsync(token, + It.IsAny(), It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_AdHocAuthorizationIsAutomaticallyCreated() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(CreateApplicationManager(mock => + { + var application = new OpenIddictApplication(); + + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Public); + + mock.Setup(manager => manager.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Code, + }); + + // Assert + Assert.NotNull(response.Code); + + Mock.Get(manager).Verify(manager => manager.CreateAsync( + It.Is(descriptor => + descriptor.ApplicationId == "3E228451-1555-46F7-A471-951EFBA23A56" && + descriptor.Subject == "Bob le Magnifique" && + descriptor.Type == AuthorizationTypes.AdHoc), + It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ProcessSignIn_AdHocAuthorizationIsNotCreatedWhenAuthorizationStorageIsDisabled() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(CreateApplicationManager(mock => + { + var application = new OpenIddictApplication(); + + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Public); + + mock.Setup(manager => manager.GetIdAsync(application, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + options.Services.AddSingleton(CreateTokenManager(mock => + { + mock.Setup(manager => manager.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + })); + + options.Services.AddSingleton(manager); + + options.DisableAuthorizationStorage(); + }); + + // Act + var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Code, + }); + + // Assert + Assert.NotNull(response.Code); + + Mock.Get(manager).Verify(manager => manager.CreateAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + protected virtual void ConfigureServices(IServiceCollection services) { services.AddOpenIddict() @@ -494,11 +2905,8 @@ namespace OpenIddict.Server.FunctionalTests { builder.UseInlineHandler(context => { - var identity = new ClaimsIdentity("Bearer"); - identity.AddClaim(Claims.Subject, "Bob le Magnifique"); - - context.Principal = new ClaimsPrincipal(identity); - context.HandleAuthentication(); + context.Principal ??= new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Magnifique"); return default; }); @@ -510,11 +2918,8 @@ namespace OpenIddict.Server.FunctionalTests { builder.UseInlineHandler(context => { - var identity = new ClaimsIdentity("Bearer"); - identity.AddClaim(Claims.Subject, "Bob le Magnifique"); - - context.Principal = new ClaimsPrincipal(identity); - context.HandleAuthentication(); + context.Principal ??= new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Magnifique"); return default; });