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