diff --git a/eng/Versions.props b/eng/Versions.props index 2f06950a..54d7b593 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -40,7 +40,7 @@ 1.5.0 4.0.0 2.9.0 - 4.7.63 + 4.13.1 5.2.2 4.0.0 4.6.0 diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index 43de0a2a..12754f90 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -1886,6 +1886,15 @@ namespace OpenIddict.Abstractions public static ClaimsPrincipal SetInternalTokenId([NotNull] this ClaimsPrincipal principal, string identifier) => principal.SetClaim(Claims.Private.TokenId, identifier); + /// + /// Sets the token usage associated with the claims principal. + /// + /// The claims principal. + /// The token usage to store. + /// The claims principal. + public static ClaimsPrincipal SetTokenUsage([NotNull] this ClaimsPrincipal principal, string usage) + => principal.SetClaim(Claims.Private.TokenUsage, usage); + private static IEnumerable GetValues(string source, char[] separators) { Debug.Assert(!string.IsNullOrEmpty(source), "The source string shouldn't be null or empty."); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index ce23c0f4..f2990025 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -297,6 +297,15 @@ namespace OpenIddict.Server context.SkipRequest(); return; } + + else if (@event.IsRejected) + { + context.Reject( + error: @event.Error ?? Errors.InvalidGrant, + description: @event.ErrorDescription, + uri: @event.ErrorUri); + return; + } } throw new InvalidOperationException(new StringBuilder() diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs index 9ea6b02c..2207a761 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs @@ -294,6 +294,15 @@ namespace OpenIddict.Server return; } + else if (@event.IsRejected) + { + context.Reject( + error: @event.Error ?? Errors.InvalidGrant, + description: @event.ErrorDescription, + uri: @event.ErrorUri); + return; + } + throw new InvalidOperationException(new StringBuilder() .Append("The device request was not handled. To handle device requests, ") .Append("create a class implementing 'IOpenIddictServerHandler' ") @@ -1059,6 +1068,15 @@ namespace OpenIddict.Server context.SkipRequest(); return; } + + else if (@event.IsRejected) + { + context.Reject( + error: @event.Error ?? Errors.InvalidGrant, + description: @event.ErrorDescription, + uri: @event.ErrorUri); + return; + } } throw new InvalidOperationException(new StringBuilder() diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index b605d24c..4c60ba4f 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -268,7 +268,7 @@ namespace OpenIddict.Server else if (notification.IsRejected) { context.Reject( - error: notification.Error ?? Errors.InvalidRequest, + error: notification.Error ?? Errors.InvalidGrant, description: notification.ErrorDescription, uri: notification.ErrorUri); return; @@ -295,6 +295,15 @@ namespace OpenIddict.Server context.SkipRequest(); return; } + + else if (@event.IsRejected) + { + context.Reject( + error: @event.Error ?? Errors.InvalidGrant, + description: @event.ErrorDescription, + uri: @event.ErrorUri); + return; + } } throw new InvalidOperationException(new StringBuilder() @@ -1641,7 +1650,8 @@ namespace OpenIddict.Server else if (string.Equals(method, CodeChallengeMethods.Sha256, StringComparison.Ordinal)) { using var algorithm = SHA256.Create(); - data = algorithm.ComputeHash(Encoding.ASCII.GetBytes(context.Request.CodeVerifier)); + data = Encoding.ASCII.GetBytes(Base64UrlEncoder.Encode( + algorithm.ComputeHash(Encoding.ASCII.GetBytes(context.Request.CodeVerifier)))); } else @@ -1657,7 +1667,7 @@ namespace OpenIddict.Server // Compare the verifier and the code challenge: if the two don't match, return an error. // Note: to prevent timing attacks, a time-constant comparer is always used. - if (!FixedTimeEquals(data, Base64UrlEncoder.DecodeBytes(challenge))) + if (!FixedTimeEquals(data, Encoding.UTF8.GetBytes(challenge))) { context.Logger.LogError("The token request was rejected because the 'code_verifier' was invalid."); @@ -1721,7 +1731,12 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (!context.Request.IsAuthorizationCodeGrantType() || string.IsNullOrEmpty(context.Request.Scope)) + if (string.IsNullOrEmpty(context.Request.Scope)) + { + return default; + } + + if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) { return default; } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs index 3c691a57..50b7dfcd 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs @@ -270,6 +270,15 @@ namespace OpenIddict.Server return; } + else if (@event.IsRejected) + { + context.Reject( + error: @event.Error ?? Errors.InvalidRequest, + description: @event.ErrorDescription, + uri: @event.ErrorUri); + return; + } + throw new InvalidOperationException(new StringBuilder() .Append("The logout request was not handled. To handle logout requests, ") .Append("create a class implementing 'IOpenIddictServerHandler' ") diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index fdb72119..cc993956 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -349,13 +349,13 @@ namespace OpenIddict.Server description: context.EndpointType switch { OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => "The specified authorization code is not valid.", + => "The specified authorization code is invalid.", OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => "The specified device code is not valid.", + => "The specified device code is invalid.", OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => "The specified refresh token is not valid.", + => "The specified refresh token is invalid.", - _ => "The specified token is not valid." + _ => "The specified token is invalid." }); return; @@ -592,17 +592,17 @@ namespace OpenIddict.Server }, description: context.EndpointType switch { - OpenIddictServerEndpointType.Authorization => "The specified identity token hint is not valid.", - OpenIddictServerEndpointType.Logout => "The specified identity token hint is not valid.", + OpenIddictServerEndpointType.Authorization => "The specified identity token hint is invalid.", + OpenIddictServerEndpointType.Logout => "The specified identity token hint is invalid.", OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => "The specified authorization code is not valid.", + => "The specified authorization code is invalid.", OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => "The specified device code is not valid.", + => "The specified device code is invalid.", OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => "The specified refresh token is not valid.", + => "The specified refresh token is invalid.", - _ => "The specified token is not valid." + _ => "The specified token is invalid." }); @@ -679,13 +679,13 @@ namespace OpenIddict.Server description: context.EndpointType switch { OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => "The specified authorization code is not valid.", + => "The specified authorization code is invalid.", OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => "The specified device code is not valid.", + => "The specified device code is invalid.", OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => "The specified refresh token is not valid.", + => "The specified refresh token is invalid.", - _ => "The specified token is not valid." + _ => "The specified token is invalid." }); return; @@ -701,7 +701,11 @@ namespace OpenIddict.Server // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. if (await _tokenManager.HasStatusAsync(token, Statuses.Redeemed)) { - await TryRevokeAuthorizationChainAsync(token); + // First, mark the redeemed token submitted by the client as revoked. + await _tokenManager.TryRevokeAsync(token); + + // Then, try to revoke the authorization and the associated token entries. + await TryRevokeAuthorizationChainAsync(context.Principal.GetInternalAuthorizationId()); context.Logger.LogError("The token '{Identifier}' has already been redeemed.", identifier); @@ -786,12 +790,8 @@ namespace OpenIddict.Server .SetInternalTokenId(await _tokenManager.GetIdAsync(token)) .SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token)); - async ValueTask TryRevokeAuthorizationChainAsync(object token) + async ValueTask TryRevokeAuthorizationChainAsync(string identifier) { - // First, mark the redeemed token submitted by the client as revoked. - await _tokenManager.TryRevokeAsync(token); - - var identifier = context.Principal.GetInternalAuthorizationId(); if (context.Options.DisableAuthorizationStorage || string.IsNullOrEmpty(identifier)) { return; @@ -805,12 +805,11 @@ namespace OpenIddict.Server await _authorizationManager.TryRevokeAsync(authorization); } - await using var enumerator = _tokenManager.FindByAuthorizationIdAsync(identifier).GetAsyncEnumerator(); - while (await enumerator.MoveNextAsync()) + await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier)) { // Don't change the status of the token used in the token request. if (string.Equals(context.Principal.GetInternalTokenId(), - await _tokenManager.GetIdAsync(enumerator.Current), StringComparison.Ordinal)) + await _tokenManager.GetIdAsync(token), StringComparison.Ordinal)) { continue; } diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs index d345aef8..68dc2d72 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs @@ -2972,6 +2972,34 @@ namespace OpenIddict.Abstractions.Tests.Primitives Assert.Equal(identifier, principal.GetClaim(Claims.Private.TokenId)); } + [Fact] + public void SetTokenUsage_ThrowsAnExceptionForNullPrincipal() + { + // Arrange + var principal = (ClaimsPrincipal) null; + + // Act and assert + var exception = Assert.Throws(() => principal.SetTokenUsage(null)); + + Assert.Equal("principal", exception.ParamName); + } + + [Theory] + [InlineData(null)] + [InlineData("access_token")] + public void SetTokenUsage_AddsUsage(string usage) + { + // Arrange + var identity = new ClaimsIdentity(); + var principal = new ClaimsPrincipal(identity); + + // Act + principal.SetTokenUsage(usage); + + // Assert + Assert.Equal(usage, principal.GetClaim(Claims.Private.TokenUsage)); + } + private TimeSpan? ParseLifeTime(string lifetime) { var lifeT = lifetime != null diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs new file mode 100644 index 00000000..d5635587 --- /dev/null +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.Net.Http.Headers; +using OpenIddict.Abstractions; +using OpenIddict.Server.FunctionalTests; +using Xunit; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.AspNetCore.FunctionalTests +{ + public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests + { + [Fact] + public async Task ExtractTokenRequest_MultipleClientCredentialsCauseAnError() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var request = context.Transaction.GetHttpRequest(); + request.Headers[HeaderNames.Authorization] = "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW"; + + return default; + }); + + builder.SetOrder(int.MinValue); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("Multiple client credentials cannot be specified.", response.ErrorDescription); + } + } +} diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs index 9e78afd5..355a2538 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs @@ -131,7 +131,7 @@ namespace OpenIddict.Server.FunctionalTests { context.Transaction.SetProperty("custom_response", new { - name = "Bob le Magnifique" + name = "Bob le Bricoleur" }); context.HandleRequest(); @@ -144,7 +144,7 @@ namespace OpenIddict.Server.FunctionalTests var response = await client.GetAsync("/connect/authorize"); // Assert - Assert.Equal("Bob le Magnifique", (string) response["name"]); + Assert.Equal("Bob le Bricoleur", (string) response["name"]); } [Fact] @@ -538,13 +538,13 @@ namespace OpenIddict.Server.FunctionalTests var application = new OpenIddictApplication(); mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(ClientTypes.Public)); + .ReturnsAsync(ClientTypes.Public); })); options.Services.AddSingleton(CreateScopeManager(mock => @@ -581,13 +581,13 @@ namespace OpenIddict.Server.FunctionalTests var application = new OpenIddictApplication(); mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(ClientTypes.Public)); + .ReturnsAsync(ClientTypes.Public); })); options.Services.AddSingleton(CreateApplicationManager(mock => @@ -595,13 +595,13 @@ namespace OpenIddict.Server.FunctionalTests var application = new OpenIddictApplication(); mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(ClientTypes.Public)); + .ReturnsAsync(ClientTypes.Public); })); options.RegisterScopes("registered_scope"); @@ -637,13 +637,13 @@ namespace OpenIddict.Server.FunctionalTests var application = new OpenIddictApplication(); mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(ClientTypes.Public)); + .ReturnsAsync(ClientTypes.Public); })); options.Services.AddSingleton(CreateScopeManager(mock => @@ -654,7 +654,7 @@ namespace OpenIddict.Server.FunctionalTests .Returns(new[] { scope }.ToAsyncEnumerable()); mock.Setup(manager => manager.GetNameAsync(scope, It.IsAny())) - .Returns(new ValueTask("scope_registered_in_database")); + .ReturnsAsync("scope_registered_in_database"); })); options.RegisterScopes("scope_registered_in_options"); @@ -859,7 +859,7 @@ namespace OpenIddict.Server.FunctionalTests { context.Transaction.SetProperty("custom_response", new { - name = "Bob le Magnifique" + name = "Bob le Bricoleur" }); context.HandleRequest(); @@ -878,7 +878,7 @@ namespace OpenIddict.Server.FunctionalTests }); // Assert - Assert.Equal("Bob le Magnifique", (string) response["name"]); + Assert.Equal("Bob le Bricoleur", (string) response["name"]); } [Fact] @@ -972,7 +972,7 @@ namespace OpenIddict.Server.FunctionalTests var manager = CreateApplicationManager(mock => { mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(result: null)); + .ReturnsAsync(value: null); }); var client = CreateClient(options => @@ -1008,10 +1008,10 @@ namespace OpenIddict.Server.FunctionalTests var manager = CreateApplicationManager(mock => { mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) - .Returns(new ValueTask(ClientTypes.Confidential)); + .ReturnsAsync(ClientTypes.Confidential); }); var client = CreateClient(options => @@ -1034,7 +1034,7 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal("The specified 'response_type' parameter is not valid for this client application.", response.ErrorDescription); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } [Fact] @@ -1046,14 +1046,14 @@ namespace OpenIddict.Server.FunctionalTests var manager = CreateApplicationManager(mock => { mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); mock.Setup(manager => manager.HasPermissionAsync(application, Permissions.Endpoints.Authorization, It.IsAny())) - .Returns(new ValueTask(false)); + .ReturnsAsync(false); }); var client = CreateClient(options => @@ -1077,7 +1077,7 @@ namespace OpenIddict.Server.FunctionalTests Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, - Permissions.Endpoints.Authorization, It.IsAny()), Times.AtLeastOnce()); + Permissions.Endpoints.Authorization, It.IsAny()), Times.Once()); } [Theory] @@ -1118,15 +1118,15 @@ namespace OpenIddict.Server.FunctionalTests var manager = CreateApplicationManager(mock => { mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); foreach (var permission in permissions) { mock.Setup(manager => manager.HasPermissionAsync(application, permission, It.IsAny())) - .Returns(new ValueTask(false)); + .ReturnsAsync(false); } }); @@ -1152,7 +1152,7 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal(description, response.ErrorDescription); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, permissions[0], It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, permissions[0], It.IsAny()), Times.Once()); } [Fact] @@ -1164,18 +1164,18 @@ namespace OpenIddict.Server.FunctionalTests var manager = CreateApplicationManager(mock => { mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); mock.Setup(manager => manager.HasPermissionAsync(application, Permissions.GrantTypes.AuthorizationCode, It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); mock.Setup(manager => manager.HasPermissionAsync(application, Permissions.GrantTypes.RefreshToken, It.IsAny())) - .Returns(new ValueTask(false)); + .ReturnsAsync(false); }); var client = CreateClient(options => @@ -1199,7 +1199,7 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal("The client application is not allowed to use the 'offline_access' scope.", response.ErrorDescription); Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, - Permissions.GrantTypes.RefreshToken, It.IsAny()), Times.AtLeastOnce()); + Permissions.GrantTypes.RefreshToken, It.IsAny()), Times.Once()); } [Fact] @@ -1211,10 +1211,10 @@ namespace OpenIddict.Server.FunctionalTests var manager = CreateApplicationManager(mock => { mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(false)); + .ReturnsAsync(false); }); var client = CreateClient(options => @@ -1235,7 +1235,7 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal("The specified 'redirect_uri' parameter is not valid for this client application.", response.ErrorDescription); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.Once()); } [Fact] @@ -1247,20 +1247,18 @@ namespace OpenIddict.Server.FunctionalTests var manager = CreateApplicationManager(mock => { mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .Returns(new ValueTask(application)); + .ReturnsAsync(application); mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) - .Returns(new ValueTask(true)); + .ReturnsAsync(true); mock.Setup(manager => manager.HasPermissionAsync(application, - Permissions.Prefixes.Scope + - Scopes.Profile, It.IsAny())) - .Returns(new ValueTask(true)); + Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny())) + .ReturnsAsync(true); mock.Setup(manager => manager.HasPermissionAsync(application, - Permissions.Prefixes.Scope + - Scopes.Email, It.IsAny())) - .Returns(new ValueTask(false)); + Permissions.Prefixes.Scope + Scopes.Email, It.IsAny())) + .ReturnsAsync(false); }); var client = CreateClient(options => @@ -1284,17 +1282,13 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal("This client application is not allowed to use the specified scope.", response.ErrorDescription); Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, - Permissions.Prefixes.Scope + - Scopes.OpenId, It.IsAny()), Times.Never()); + Permissions.Prefixes.Scope + Scopes.OpenId, It.IsAny()), Times.Never()); Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, - Permissions.Prefixes.Scope + - Scopes.OfflineAccess, It.IsAny()), Times.Never()); + Permissions.Prefixes.Scope + Scopes.OfflineAccess, It.IsAny()), Times.Never()); Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, - Permissions.Prefixes.Scope + - Scopes.Profile, It.IsAny()), Times.AtLeastOnce()); + Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, - Permissions.Prefixes.Scope + - Scopes.Email, It.IsAny()), Times.AtLeastOnce()); + Permissions.Prefixes.Scope + Scopes.Email, It.IsAny()), Times.Once()); } [Theory] @@ -1349,7 +1343,7 @@ namespace OpenIddict.Server.FunctionalTests { context.Transaction.SetProperty("custom_response", new { - name = "Bob le Magnifique" + name = "Bob le Bricoleur" }); context.HandleRequest(); @@ -1368,7 +1362,7 @@ namespace OpenIddict.Server.FunctionalTests }); // Assert - Assert.Equal("Bob le Magnifique", (string) response["name"]); + Assert.Equal("Bob le Bricoleur", (string) response["name"]); } [Fact] @@ -1452,7 +1446,7 @@ namespace OpenIddict.Server.FunctionalTests { context.Transaction.SetProperty("custom_response", new { - name = "Bob le Magnifique" + name = "Bob le Bricoleur" }); context.HandleRequest(); @@ -1471,7 +1465,7 @@ namespace OpenIddict.Server.FunctionalTests }); // Assert - Assert.Equal("Bob le Magnifique", (string) response["name"]); + Assert.Equal("Bob le Bricoleur", (string) response["name"]); } [Fact] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs new file mode 100644 index 00000000..06730cb4 --- /dev/null +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -0,0 +1,3019 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using OpenIddict.Abstractions; +using Xunit; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers; + +namespace OpenIddict.Server.FunctionalTests +{ + public abstract partial class OpenIddictServerIntegrationTests + { + [Theory] + [InlineData(nameof(HttpMethod.Delete))] + [InlineData(nameof(HttpMethod.Get))] + [InlineData(nameof(HttpMethod.Head))] + [InlineData(nameof(HttpMethod.Options))] + [InlineData(nameof(HttpMethod.Put))] + [InlineData(nameof(HttpMethod.Trace))] + public async Task ExtractTokenRequest_UnexpectedMethodReturnsAnError(string method) + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.SendAsync(method, "/connect/token", new OpenIddictRequest()); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The specified HTTP method is not valid.", response.ErrorDescription); + } + + [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 ExtractTokenRequest_AllowsRejectingRequest(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()); + + // Assert + Assert.Equal(error ?? Errors.InvalidRequest, response.Error); + Assert.Equal(description, response.ErrorDescription); + Assert.Equal(uri, response.ErrorUri); + } + + [Fact] + public async Task ExtractTokenRequest_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()); + + // Assert + Assert.Equal("Bob le Bricoleur", (string) response["name"]); + } + + [Fact] + public async Task ExtractTokenRequest_AllowsSkippingHandler() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest()); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response["name"]); + } + + [Fact] + public async Task ValidateTokenRequest_MissingGrantTypeCausesAnError() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = null + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'grant_type' parameter is missing.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_MissingClientIdCausesAnErrorForCodeFlowRequests() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = null, + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_MissingCodeCausesAnError() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = null, + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'code' parameter is missing.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_MissingRefreshTokenCausesAnError() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = null + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'refresh_token' parameter is missing.", response.ErrorDescription); + } + + [Theory] + [InlineData(null, null)] + [InlineData("client_id", null)] + [InlineData(null, "client_secret")] + public async Task ValidateTokenRequest_MissingClientCredentialsCauseAnError(string identifier, string secret) + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = identifier, + ClientSecret = secret, + GrantType = GrantTypes.ClientCredentials + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The 'client_id' and 'client_secret' parameters are " + + "required when using the client credentials grant.", response.ErrorDescription); + } + + [Theory] + [InlineData(null, null)] + [InlineData("username", null)] + [InlineData(null, "password")] + public async Task ValidateTokenRequest_MissingUserCredentialsCauseAnError(string username, string password) + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = username, + Password = password + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'username' and/or 'password' parameters are missing.", response.ErrorDescription); + } + + [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 ValidateTokenRequest_AllowsRejectingRequest(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 ValidateTokenRequest_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 ValidateTokenRequest_AllowsSkippingHandler() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response["name"]); + } + + [Fact] + public async Task ValidateTokenRequest_InvalidAuthorizationCodeCausesAnError() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_InvalidRefreshTokenCausesAnError() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // 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 invalid.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_ExpiredAuthorizationCodeCausesAnError() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetExpirationDate(DateTimeOffset.UtcNow - TimeSpan.FromDays(1)); + + 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 + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_ExpiredRefreshTokenCausesAnError() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetExpirationDate(DateTimeOffset.UtcNow - TimeSpan.FromDays(1)); + + 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.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenPresentersAreMissing() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters(Enumerable.Empty()); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = GrantTypes.AuthorizationCode + }); + }); + + Assert.Equal("The presenters list cannot be extracted from the authorization code.", exception.Message); + } + + [Fact] + public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenCallerIsNotAPresenter() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Contoso"); + + 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 + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code cannot be used by this client application.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RefreshTokenCausesAnErrorWhenCallerIsNotAPresenter() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Contoso"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token cannot be used by this client application.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenRedirectUriIsMissing() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetClaim(Claims.Private.RedirectUri, "http://www.fabrikam.com/callback"); + + 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 = null + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'redirect_uri' parameter is missing.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenRedirectUriIsInvalid() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetClaim(Claims.Private.RedirectUri, "http://www.fabrikam.com/callback"); + + 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.contoso.com/redirect_uri" + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified 'redirect_uri' parameter doesn't match the client " + + "redirection endpoint the authorization code was initially sent to.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenCodeVerifierIsMissing() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetClaim(Claims.Private.CodeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") + .SetClaim(Claims.Private.CodeChallengeMethod, CodeChallengeMethods.Sha256); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + CodeVerifier = null, + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'code_verifier' parameter is missing.", response.ErrorDescription); + } + + [Theory] + [InlineData(CodeChallengeMethods.Plain, "challenge", "invalid_verifier")] + [InlineData(CodeChallengeMethods.Sha256, "challenge", "invalid_verifier")] + public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenCodeVerifierIsInvalid(string method, string challenge, string verifier) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetClaim(Claims.Private.CodeChallenge, challenge) + .SetClaim(Claims.Private.CodeChallengeMethod, method); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + CodeVerifier = verifier, + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified 'code_verifier' parameter is invalid.", response.ErrorDescription); + } + + [Theory] + [InlineData(CodeChallengeMethods.Plain, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")] + [InlineData(CodeChallengeMethods.Sha256, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")] + public async Task ValidateTokenRequest_TokenRequestSucceedsWhenCodeVerifierIsValid(string method, string challenge, string verifier) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Magnifique") + .SetPresenters("Fabrikam") + .SetClaim(Claims.Private.CodeChallenge, challenge) + .SetClaim(Claims.Private.CodeChallengeMethod, method); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + CodeVerifier = verifier, + GrantType = GrantTypes.AuthorizationCode + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenScopeIsUnexpected() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetScopes(Enumerable.Empty()); + + 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, + Scope = "profile phone" + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The 'scope' parameter is not valid in this context.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenScopeIsInvalid() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetScopes("profile", "email"); + + 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, + Scope = "profile phone" + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified 'scope' parameter is invalid.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RefreshTokenCausesAnErrorWhenScopeIsUnexpected() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes(Enumerable.Empty()); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8", + Scope = "profile phone" + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The 'scope' parameter is not valid in this context.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RefreshTokenCausesAnErrorWhenScopeIsInvalid() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetScopes("profile", "email"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8", + Scope = "profile phone" + }); + + // Assert + Assert.Equal(Errors.InvalidGrant, response.Error); + Assert.Equal("The specified 'scope' parameter is invalid.", response.ErrorDescription); + } + + [Theory] + [InlineData(GrantTypes.AuthorizationCode)] + [InlineData(GrantTypes.ClientCredentials)] + [InlineData(GrantTypes.Password)] + [InlineData(GrantTypes.RefreshToken)] + public async Task ValidateTokenRequest_RequestIsRejectedWhenFlowIsNotEnabled(string flow) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.GrantTypes.Remove(flow)); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = flow, + Username = "johndoe", + Password = "A3ddj3w", + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(Errors.UnsupportedGrantType, response.Error); + Assert.Equal("The specified 'grant_type' parameter is not supported.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenFlowIsDisabled() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.GrantTypes.Remove(GrantTypes.RefreshToken)); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = Scopes.OfflineAccess + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The 'offline_access' scope is not allowed.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenUnregisteredScopeIsSpecified() + { + // Arrange + var client = CreateClient(options => + { + options.Services.AddSingleton(CreateScopeManager(mock => + { + mock.Setup(manager => manager.FindByNamesAsync( + It.Is>(scopes => scopes.Length == 1 && scopes[0] == "unregistered_scope"), + It.IsAny())) + .Returns(AsyncEnumerable.Empty()); + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = "unregistered_scope" + }); + + // Assert + Assert.Equal(Errors.InvalidScope, response.Error); + Assert.Equal("The specified 'scope' parameter is not valid.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsValidatedWhenScopeRegisteredInOptionsIsSpecified() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.RegisterScopes("registered_scope"); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = "registered_scope" + }); + + // Assert + Assert.Null(response.Error); + Assert.Null(response.ErrorDescription); + Assert.Null(response.ErrorUri); + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsValidatedWhenRegisteredScopeIsSpecified() + { + // Arrange + var scope = new OpenIddictScope(); + + var manager = CreateScopeManager(mock => + { + mock.Setup(manager => manager.FindByNamesAsync( + It.Is>(scopes => scopes.Length == 1 && scopes[0] == "scope_registered_in_database"), + It.IsAny())) + .Returns(new[] { scope }.ToAsyncEnumerable()); + + mock.Setup(manager => manager.GetNameAsync(scope, It.IsAny())) + .ReturnsAsync("scope_registered_in_database"); + }); + + var client = CreateClient(options => + { + options.RegisterScopes("scope_registered_in_options"); + + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = "scope_registered_in_database scope_registered_in_options" + }); + + // Assert + Assert.Null(response.Error); + Assert.Null(response.ErrorDescription); + Assert.Null(response.ErrorUri); + Assert.NotNull(response.AccessToken); + } + + [Theory] + [InlineData("client_id", "")] + [InlineData("", "client_secret")] + public async Task ValidateTokenRequest_ClientCredentialsRequestIsRejectedWhenCredentialsAreMissing(string identifier, string secret) + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = identifier, + ClientSecret = secret, + GrantType = GrantTypes.ClientCredentials + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The 'client_id' and 'client_secret' parameters are " + + "required when using the client credentials grant.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.Configure(options => options.AcceptAnonymousClients = false); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = null, + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The mandatory 'client_id' parameter is missing.", response.ErrorDescription); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCannotBeFound() + { + // Arrange + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(value: null); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal("The specified 'client_id' parameter is invalid.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.HasPermissionAsync(application, + Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + + options.Configure(options => options.IgnoreEndpointPermissions = false); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.UnauthorizedClient, response.Error); + Assert.Equal("This client application is not allowed to use the token endpoint.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, + Permissions.Endpoints.Token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenGrantTypePermissionIsNotGranted() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.HasPermissionAsync(application, + Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + + options.Configure(options => options.IgnoreGrantTypePermissions = false); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.UnauthorizedClient, response.Error); + Assert.Equal("This client application is not allowed to use the specified grant type.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, + Permissions.GrantTypes.Password, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_RequestWithOfflineAccessScopeIsRejectedWhenRefreshTokenPermissionIsNotGranted() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.HasPermissionAsync(application, + Permissions.GrantTypes.Password, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.HasPermissionAsync(application, + Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + + options.Configure(options => options.IgnoreGrantTypePermissions = false); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = Scopes.OfflineAccess + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The client application is not allowed to use the 'offline_access' scope.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, + Permissions.GrantTypes.RefreshToken, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_ClientCredentialsRequestFromPublicClientIsRejected() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Public); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.ClientCredentials + }); + + // Assert + Assert.Equal(Errors.UnauthorizedClient, response.Error); + Assert.Equal("The specified 'grant_type' parameter is not valid for this client application.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenScopePermissionIsNotGranted() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Public); + + mock.Setup(manager => manager.HasPermissionAsync(application, + Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.HasPermissionAsync(application, + Permissions.Prefixes.Scope + Scopes.Email, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + + options.RegisterScopes(Scopes.Email, Scopes.Profile); + options.Configure(options => options.IgnoreScopePermissions = false); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w", + Scope = "openid offline_access profile email" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("This client application is not allowed to use the specified scope.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, + Permissions.Prefixes.Scope + Scopes.OpenId, It.IsAny()), Times.Never()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, + Permissions.Prefixes.Scope + Scopes.OfflineAccess, It.IsAny()), Times.Never()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, + Permissions.Prefixes.Scope + Scopes.Profile, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, + Permissions.Prefixes.Scope + Scopes.Email, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_ClientSecretCannotBeUsedByPublicClients() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Public); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The 'client_secret' parameter is not valid for this client application.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_ClientSecretIsRequiredForConfidentialClients() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Confidential); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = null, + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_ClientSecretIsRequiredForHybridClients() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Hybrid); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = null, + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal("The 'client_secret' parameter required for this client application is missing.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateTokenRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() + { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(ClientTypes.Confidential); + + mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal("The specified client credentials are invalid.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.GetClientTypeAsync(application, It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenStorageIsDisabled() + { + // Arrange + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + + 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.SetRevocationEndpointUris(Array.Empty()); + options.DisableTokenStorage(); + options.DisableSlidingExpiration(); + }); + + // 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); + } + + [Fact] + public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenStorageIsDisabled() + { + // Arrange + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.SetRevocationEndpointUris(Array.Empty()); + options.DisableTokenStorage(); + options.DisableSlidingExpiration(); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsUnknown() + { + // Arrange + var manager = CreateTokenManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(value: null); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + + 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 invalid.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsUnknown() + { + // Arrange + var manager = CreateTokenManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(value: null); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + + 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 invalid.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsAlreadyRedeemed() + { + // 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.Redeemed, It.IsAny())) + .ReturnsAsync(true); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + + 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 has already been redeemed.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsAlreadyRedeemed() + { + // 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(true); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + + 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 has already been redeemed.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RevokesAuthorizationWhenAuthorizationCodeIsAlreadyRedeemed() + { + // Arrange + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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(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.GetAuthorizationIdAsync(token, It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .Returns(AsyncEnumerable.Empty()); + })); + + 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 has already been redeemed.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(authorization, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RevokesAuthorizationWhenRefreshTokenIsAlreadyRedeemed() + { + // Arrange + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + return default; + }); + + 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.GetAuthorizationIdAsync(token, It.IsAny())) + .ReturnsAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Redeemed, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.FindByAuthorizationIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .Returns(AsyncEnumerable.Empty()); + })); + + 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 has already been redeemed.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(authorization, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RevokesTokensWhenAuthorizationCodeIsAlreadyRedeemed() + { + // Arrange + var tokens = ImmutableArray.Create( + new OpenIddictToken(), + new OpenIddictToken(), + new OpenIddictToken()); + + var manager = CreateTokenManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(tokens[0]); + + mock.Setup(manager => manager.GetIdAsync(tokens[0], It.IsAny())) + .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); + + mock.Setup(manager => manager.GetIdAsync(tokens[1], It.IsAny())) + .ReturnsAsync("47468A64-C9A7-49C7-939C-19CC0F5DD166"); + + 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(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("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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 has already been redeemed.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny()), Times.Once()); + 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 HandleTokenRequest_RevokesTokensWhenRefreshTokenIsAlreadyRedeemed() + { + // Arrange + var tokens = ImmutableArray.Create( + 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("47468A64-C9A7-49C7-939C-19CC0F5DD166"); + + 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(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(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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 has already been redeemed.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(tokens[0], Statuses.Redeemed, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.TryRevokeAsync(tokens[0], It.IsAny()), Times.Once()); + 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 HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsInvalid() + { + // 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.Redeemed, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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.HasStatusAsync(token, Statuses.Redeemed, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasStatusAsync(token, Statuses.Valid, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenRefreshTokenIsInvalid() + { + // 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(false); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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.HasStatusAsync(token, 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()); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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(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.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); + })); + + options.Services.AddSingleton(manager); + + options.DisableAuthorizationStorage(); + }); + + // 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()); + }); + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + return default; + }); + + 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.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); + + options.DisableAuthorizationStorage(); + }); + + // 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_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeCannotBeFound() + { + // Arrange + var manager = CreateAuthorizationManager(mock => + { + mock.Setup(manager => manager.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(value: null); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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(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.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); + }); + + // 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 authorization associated with the authorization code is no longer valid.", response.ErrorDescription); + + 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); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(TokenUsages.AuthorizationCode, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetPresenters("Fabrikam") + .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + 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(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.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); + }); + + // 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 authorization associated with the authorization code is no longer valid.", response.ErrorDescription); + + 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); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + return default; + }); + + 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.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); + }); + + // 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 authorization associated with the refresh token is no longer valid.", response.ErrorDescription); + + 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); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal(TokenUsages.RefreshToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + return default; + }); + + 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.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); + }); + + // 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 authorization associated with the refresh token is no longer valid.", response.ErrorDescription); + + 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()); + } + + [Theory] + [InlineData(GrantTypes.AuthorizationCode)] + [InlineData(GrantTypes.ClientCredentials)] + [InlineData(GrantTypes.Password)] + [InlineData(GrantTypes.RefreshToken)] + [InlineData("urn:ietf:params:oauth:grant-type:custom_grant")] + public async Task HandleTokenRequest_RequestsAreSuccessfullyHandled(string flow) + { + // Arrange + var manager = CreateTokenManager(mock => + { + var token = new OpenIddictToken(); + + mock.Setup(manager => manager.FindByIdAsync("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC", It.IsAny())) + .ReturnsAsync(token); + + mock.Setup(manager => manager.GetIdAsync(token, It.IsAny())) + .ReturnsAsync("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC"); + + 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); + }); + + var client = CreateClient(options => + { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Bricoleur") + .SetPresenters("Fabrikam") + .SetInternalTokenId("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC"); + + if (context.Request.IsAuthorizationCodeGrantType()) + { + context.Principal.SetPresenters("Fabrikam"); + } + + 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.Confidential); + + mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + options.Services.AddSingleton(manager); + + options.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); + options.DisableAuthorizationStorage(); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Code = "8xLOxBtZp8", + GrantType = flow, + RedirectUri = "http://www.fabrikam.com/path", + RefreshToken = "8xLOxBtZp8", + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [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 HandleTokenRequest_AllowsRejectingRequest(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.InvalidGrant, response.Error); + Assert.Equal(description, response.ErrorDescription); + Assert.Equal(uri, response.ErrorUri); + } + + [Fact] + public async Task HandleTokenRequest_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 HandleTokenRequest_AllowsSkippingHandler() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response["name"]); + } + + [Fact] + public async Task ApplyTokenResponse_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 ApplyTokenResponse_ResponseContainsCustomParameters() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Response["custom_parameter"] = "custom_value"; + context.Response["parameter_with_multiple_values"] = new[] + { + "custom_value_1", + "custom_value_2" + }; + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal("custom_value", (string) response["custom_parameter"]); + Assert.Equal(new[] { "custom_value_1", "custom_value_2" }, (string[]) response["parameter_with_multiple_values"]); + } + } +} diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 842f4182..1c63fe50 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -101,6 +101,22 @@ namespace OpenIddict.Server.FunctionalTests builder.SetOrder(int.MaxValue); }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var identity = new ClaimsIdentity("Bearer"); + identity.AddClaim(Claims.Subject, "Bob le Magnifique"); + + context.Principal = new ClaimsPrincipal(identity); + context.HandleAuthentication(); + + return default; + }); + + builder.SetOrder(int.MaxValue); + }); }); } diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs new file mode 100644 index 00000000..8b302c8b --- /dev/null +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Threading.Tasks; +using OpenIddict.Abstractions; +using OpenIddict.Server.FunctionalTests; +using Owin; +using Xunit; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.Owin.FunctionalTests +{ + public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests + { + [Fact] + public async Task ExtractTokenRequest_MultipleClientCredentialsCauseAnError() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var request = context.Transaction.GetOwinRequest(); + request.Headers["Authorization"] = "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW"; + + return default; + }); + + builder.SetOrder(int.MinValue); + }); + }); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("Multiple client credentials cannot be specified.", response.ErrorDescription); + } + } +}