diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index e6305361..e68662d4 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -95,6 +95,10 @@ namespace Microsoft.AspNetCore.Builder { "client credentials, password and refresh token flows."); } + if (options.RevocationEndpointPath.HasValue && options.DisableTokenRevocation) { + throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled."); + } + return app.UseOpenIdConnectServer(options); } @@ -479,6 +483,21 @@ namespace Microsoft.AspNetCore.Builder { return builder.Configure(options => options.UseSlidingExpiration = false); } + /// + /// Disables token revocation, so that authorization code and + /// refresh tokens are never stored and cannot be revoked. + /// Using this option is generally not recommended. + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder DisableTokenRevocation([NotNull] this OpenIddictBuilder builder) { + if (builder == null) { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.Configure(options => options.DisableTokenRevocation = true); + } + /// /// Enables the authorization endpoint. /// diff --git a/src/OpenIddict/OpenIddictOptions.cs b/src/OpenIddict/OpenIddictOptions.cs index 59a7c218..8a18c3f8 100644 --- a/src/OpenIddict/OpenIddictOptions.cs +++ b/src/OpenIddict/OpenIddictOptions.cs @@ -28,6 +28,13 @@ namespace OpenIddict { /// public IDistributedCache Cache { get; set; } + /// + /// Gets or sets a boolean indicating whether token revocation should be disabled. + /// When disabled, authorization code and refresh tokens are not stored + /// and cannot be revoked. Using this option is generally not recommended. + /// + public bool DisableTokenRevocation { get; set; } + /// /// Gets or sets a boolean indicating whether request caching should be enabled. /// When enabled, both authorization and logout requests are automatically stored diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs index 330c9282..f09d11cf 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -182,58 +182,55 @@ namespace OpenIddict { } public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context) { + var options = context.HttpContext.RequestServices.GetRequiredService>(); var logger = context.HttpContext.RequestServices.GetRequiredService>>(); var tokens = context.HttpContext.RequestServices.GetRequiredService>(); - if (context.Request.IsAuthorizationCodeGrantType()) { + if (!options.Value.DisableTokenRevocation && (context.Request.IsAuthorizationCodeGrantType() || + context.Request.IsRefreshTokenGrantType())) { Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); - // Extract the token identifier from the authorization code. + // Extract the token identifier from the authentication ticket. var identifier = context.Ticket.GetTicketId(); Debug.Assert(!string.IsNullOrEmpty(identifier), - "The authorization code should contain a ticket identifier."); + "The authentication ticket should contain a ticket identifier."); - // Retrieve the token from the database and ensure it is still valid. - var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); - if (token == null) { - logger.LogError("The token request was rejected because the authorization code was revoked."); + if (context.Request.IsAuthorizationCodeGrantType()) { + // Retrieve the token from the database and ensure it is still valid. + var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + if (token == null) { + logger.LogError("The token request was rejected because the authorization code was revoked."); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The authorization code is no longer valid."); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The authorization code is no longer valid."); - return; - } + return; + } - // Revoke the authorization code to prevent token reuse. - await tokens.RevokeAsync(token, context.HttpContext.RequestAborted); - } - - else if (context.Request.IsRefreshTokenGrantType()) { - Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); - - // Extract the token identifier from the refresh token. - var identifier = context.Ticket.GetTicketId(); - Debug.Assert(!string.IsNullOrEmpty(identifier), - "The refresh token should contain a ticket identifier."); - - // Retrieve the token from the database and ensure it is still valid. - var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); - if (token == null) { - logger.LogError("The token request was rejected because the refresh token was revoked."); - - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The refresh token is no longer valid."); - - return; + // Revoke the authorization code to prevent token reuse. + await tokens.RevokeAsync(token, context.HttpContext.RequestAborted); } - // When sliding expiration is enabled, immediately - // revoke the refresh token to prevent future reuse. - // See https://tools.ietf.org/html/rfc6749#section-6. - if (context.Options.UseSlidingExpiration) { - await tokens.RevokeAsync(token, context.HttpContext.RequestAborted); + else if (context.Request.IsRefreshTokenGrantType()) { + // Retrieve the token from the database and ensure it is still valid. + var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + if (token == null) { + logger.LogError("The token request was rejected because the refresh token was revoked."); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The refresh token is no longer valid."); + + return; + } + + // When sliding expiration is enabled, immediately + // revoke the refresh token to prevent future reuse. + // See https://tools.ietf.org/html/rfc6749#section-6. + if (context.Options.UseSlidingExpiration) { + await tokens.RevokeAsync(token, context.HttpContext.RequestAborted); + } } } diff --git a/src/OpenIddict/OpenIddictProvider.Introspection.cs b/src/OpenIddict/OpenIddictProvider.Introspection.cs index 31253c66..ca5c31bb 100644 --- a/src/OpenIddict/OpenIddictProvider.Introspection.cs +++ b/src/OpenIddict/OpenIddictProvider.Introspection.cs @@ -13,6 +13,7 @@ using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using OpenIddict.Core; namespace OpenIddict { @@ -89,6 +90,7 @@ namespace OpenIddict { } public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) { + var options = context.HttpContext.RequestServices.GetRequiredService>(); var logger = context.HttpContext.RequestServices.GetRequiredService>>(); var tokens = context.HttpContext.RequestServices.GetRequiredService>(); @@ -110,7 +112,7 @@ namespace OpenIddict { } // When the received ticket is revocable, ensure it is still valid. - if (context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken()) { + if (!options.Value.DisableTokenRevocation && (context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken())) { // Retrieve the token from the database using the unique identifier stored in the authentication ticket: // if the corresponding entry cannot be found, return Active = false to indicate that is is no longer valid. var token = await tokens.FindByIdAsync(context.Ticket.GetTicketId(), context.HttpContext.RequestAborted); diff --git a/src/OpenIddict/OpenIddictProvider.Revocation.cs b/src/OpenIddict/OpenIddictProvider.Revocation.cs index 84eaab89..ba0adf04 100644 --- a/src/OpenIddict/OpenIddictProvider.Revocation.cs +++ b/src/OpenIddict/OpenIddictProvider.Revocation.cs @@ -23,6 +23,8 @@ namespace OpenIddict { var logger = context.HttpContext.RequestServices.GetRequiredService>>(); var options = context.HttpContext.RequestServices.GetRequiredService>(); + Debug.Assert(!options.Value.DisableTokenRevocation, "Token revocation support shouldn't be disabled at this stage."); + // When token_type_hint is specified, reject the request if it doesn't correspond to a revocable token. if (!string.IsNullOrEmpty(context.Request.TokenTypeHint) && !string.Equals(context.Request.TokenTypeHint, OpenIdConnectConstants.TokenTypeHints.AuthorizationCode) && diff --git a/src/OpenIddict/OpenIddictProvider.Serialization.cs b/src/OpenIddict/OpenIddictProvider.Serialization.cs index 0fcb6141..f1964163 100644 --- a/src/OpenIddict/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict/OpenIddictProvider.Serialization.cs @@ -11,37 +11,44 @@ using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Core; namespace OpenIddict { public partial class OpenIddictProvider : OpenIdConnectServerProvider where TApplication : class where TAuthorization : class where TScope : class where TToken : class { public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) { + var options = context.HttpContext.RequestServices.GetRequiredService>(); var tokens = context.HttpContext.RequestServices.GetRequiredService>(); - var identifier = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, context.HttpContext.RequestAborted); - if (string.IsNullOrEmpty(identifier)) { - throw new InvalidOperationException("The unique key associated with an authorization code cannot be null or empty."); - } + if (!options.Value.DisableTokenRevocation) { + var identifier = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, context.HttpContext.RequestAborted); + if (string.IsNullOrEmpty(identifier)) { + throw new InvalidOperationException("The unique key associated with an authorization code cannot be null or empty."); + } - // Attach the key returned by the underlying store - // to the authorization code to override the default GUID - // generated by the OpenID Connect server middleware. - context.Ticket.SetTicketId(identifier); + // Attach the key returned by the underlying store + // to the authorization code to override the default GUID + // generated by the OpenID Connect server middleware. + context.Ticket.SetTicketId(identifier); + } } public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { + var options = context.HttpContext.RequestServices.GetRequiredService>(); var tokens = context.HttpContext.RequestServices.GetRequiredService>(); - var identifier = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, context.HttpContext.RequestAborted); - if (string.IsNullOrEmpty(identifier)) { - throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); - } + if (!options.Value.DisableTokenRevocation) { + var identifier = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, context.HttpContext.RequestAborted); + if (string.IsNullOrEmpty(identifier)) { + throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); + } - // Attach the key returned by the underlying store - // to the refresh token to override the default GUID - // generated by the OpenID Connect server middleware. - context.Ticket.SetTicketId(identifier); + // Attach the key returned by the underlying store + // to the refresh token to override the default GUID + // generated by the OpenID Connect server middleware. + context.Ticket.SetTicketId(identifier); + } } } } \ No newline at end of file diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs index e5aa536a..48da126b 100644 --- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs @@ -15,7 +15,7 @@ using Xunit; namespace OpenIddict.Tests { public class OpenIddictExtensionsTests { [Fact] - public void UseOpenIddict_AnExceptionIsThrownWhenServicesAreNotRegistered() { + public void UseOpenIddict_ThrowsAnExceptionWhenServicesAreNotRegistered() { // Arrange var services = new ServiceCollection(); @@ -29,7 +29,7 @@ namespace OpenIddict.Tests { } [Fact] - public void UseOpenIddict_AnExceptionIsThrownWhenNoDistributedCacheIsRegisteredIfRequestCachingIsEnabled() { + public void UseOpenIddict_ThrowsAnExceptionWhenNoDistributedCacheIsRegisteredIfRequestCachingIsEnabled() { // Arrange var services = new ServiceCollection(); @@ -46,7 +46,7 @@ namespace OpenIddict.Tests { } [Fact] - public void UseOpenIddict_AnExceptionIsThrownWhenNoSigningCredentialsIsRegistered() { + public void UseOpenIddict_ThrowsAnExceptionWhenNoSigningCredentialsIsRegistered() { // Arrange var services = new ServiceCollection(); services.AddOpenIddict(); @@ -62,7 +62,7 @@ namespace OpenIddict.Tests { } [Fact] - public void UseOpenIddict_AnExceptionIsThrownWhenNoFlowIsEnabled() { + public void UseOpenIddict_ThrowsAnExceptionWhenNoFlowIsEnabled() { // Arrange var services = new ServiceCollection(); @@ -83,7 +83,7 @@ namespace OpenIddict.Tests { [Theory] [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] [InlineData(OpenIdConnectConstants.GrantTypes.Implicit)] - public void UseOpenIddict_AnExceptionIsThrownWhenAuthorizationEndpointIsDisabled(string flow) { + public void UseOpenIddict_ThrowsAnExceptionWhenAuthorizationEndpointIsDisabled(string flow) { // Arrange var services = new ServiceCollection(); @@ -109,7 +109,7 @@ namespace OpenIddict.Tests { [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] [InlineData(OpenIdConnectConstants.GrantTypes.Password)] [InlineData(OpenIdConnectConstants.GrantTypes.RefreshToken)] - public void UseOpenIddict_AnExceptionIsThrownWhenTokenEndpointIsDisabled(string flow) { + public void UseOpenIddict_ThrowsAnExceptionWhenTokenEndpointIsDisabled(string flow) { // Arrange var services = new ServiceCollection(); @@ -131,6 +131,29 @@ namespace OpenIddict.Tests { "client credentials, password and refresh token flows.", exception.Message); } + [Fact] + public void UseOpenIddict_ThrowsAnExceptionWhenTokenRevocationIsDisabled() { + // Arrange + var services = new ServiceCollection(); + + services.AddOpenIddict() + .AddSigningCertificate( + assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly, + resource: "OpenIddict.Tests.Certificate.pfx", + password: "OpenIddict") + .EnableAuthorizationEndpoint("/connect/authorize") + .EnableRevocationEndpoint("/connect/revocation") + .AllowImplicitFlow() + .DisableTokenRevocation(); + + var builder = new ApplicationBuilder(services.BuildServiceProvider()); + + // Act and assert + var exception = Assert.Throws(() => builder.UseOpenIddict()); + + Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message); + } + [Fact] public void Configure_OptionsAreCorrectlyAmended() { // Arrange @@ -413,6 +436,42 @@ namespace OpenIddict.Tests { Assert.Equal(PathString.Empty, options.Value.CryptographyEndpointPath); } + [Fact] + public void DisableSlidingExpiration_SlidingExpirationIsDisabled() { + // Arrange + var services = new ServiceCollection(); + services.AddOptions(); + + var builder = new OpenIddictBuilder(services); + + // Act + builder.DisableSlidingExpiration(); + + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>(); + + // Assert + Assert.False(options.Value.UseSlidingExpiration); + } + + [Fact] + public void DisableTokenRevocation_TokenRevocationIsDisabled() { + // Arrange + var services = new ServiceCollection(); + services.AddOptions(); + + var builder = new OpenIddictBuilder(services); + + // Act + builder.DisableTokenRevocation(); + + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>(); + + // Assert + Assert.True(options.Value.DisableTokenRevocation); + } + [Fact] public void EnableAuthorizationEndpoint_AuthorizationEndpointIsEnabled() { // Arrange diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs index 975e9401..f2165f44 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs @@ -7,6 +7,7 @@ using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -299,6 +300,98 @@ namespace OpenIddict.Tests { Mock.Get(manager).Verify(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); } + [Fact] + public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenRevocationIsDisabled() { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); + + builder.DisableTokenRevocation(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenRevocationIsDisabled() { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); + })); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); + + builder.DisableTokenRevocation(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.NotNull(response.AccessToken); + } + [Fact] public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationCodeIsExpired() { // Arrange diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs index efd41581..b3bb02df 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -8,6 +8,7 @@ using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; using Moq; @@ -206,6 +207,110 @@ namespace OpenIddict.Tests { Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); } + [Fact] + public async Task HandleIntrospectionRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenRevocationIsDisabled() { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); + + builder.DisableTokenRevocation(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + } + + [Fact] + public async Task HandleIntrospectionRequest_RefreshTokenRevocationIsIgnoredWhenTokenRevocationIsDisabled() { + // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); + + instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); + + builder.DisableTokenRevocation(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.True((bool) response[OpenIdConnectConstants.Claims.Active]); + } + [Fact] public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationCodeIsRevoked() { // Arrange