diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs b/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs index 8f487c78..273b1e03 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs @@ -7,6 +7,7 @@ using System; using System.ComponentModel; using System.Linq; +using System.Text; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; @@ -139,8 +140,7 @@ namespace OpenIddict.Server if (!options.AuthorizationEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) { - throw new InvalidOperationException("The authorization endpoint must be enabled to use " + - "the authorization code and implicit flows."); + throw new InvalidOperationException("The authorization endpoint must be enabled to use the authorization code and implicit flows."); } // Ensure the token endpoint has been enabled when the authorization code, @@ -150,51 +150,51 @@ namespace OpenIddict.Server options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Password) || options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))) { - throw new InvalidOperationException("The token endpoint must be enabled to use the authorization code, " + - "client credentials, password and refresh token flows."); + throw new InvalidOperationException( + "The token endpoint must be enabled to use the authorization code, client credentials, password and refresh token flows."); } - if (options.RevocationEndpointPath.HasValue && options.DisableTokenRevocation) + if (options.RevocationEndpointPath.HasValue && options.DisableTokenStorage) { - throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled."); + throw new InvalidOperationException("The revocation endpoint cannot be enabled when token storage is disabled."); } - if (options.UseReferenceTokens && options.DisableTokenRevocation) + if (options.UseReferenceTokens && options.DisableTokenStorage) { - throw new InvalidOperationException( - "Reference tokens cannot be used when disabling token revocation."); + throw new InvalidOperationException("Reference tokens cannot be used when disabling token storage."); } if (options.UseReferenceTokens && options.AccessTokenHandler != null) { - throw new InvalidOperationException( - "Reference tokens cannot be used when configuring JWT as the access token format."); + throw new InvalidOperationException("Reference tokens cannot be used when configuring JWT as the access token format."); } - if (options.UseSlidingExpiration && options.DisableTokenRevocation && !options.UseRollingTokens) + if (options.UseSlidingExpiration && options.DisableTokenStorage && !options.UseRollingTokens) { - throw new InvalidOperationException("Sliding expiration must be disabled when turning off " + - "token revocation if rolling tokens are not used."); + throw new InvalidOperationException( + "Sliding expiration must be disabled when turning off token storage if rolling tokens are not used."); } if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0) { - throw new InvalidOperationException( - "At least one signing key must be registered when using JWT as the access token format. " + - "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + - "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + - "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); + throw new InvalidOperationException(new StringBuilder() + .AppendLine("At least one signing key must be registered when using JWT as the access token format.") + .Append("Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' ") + .Append("or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call ") + .Append("'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.") + .ToString()); } // Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled. if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) && options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) { - throw new InvalidOperationException( - "At least one asymmetric signing key must be registered when enabling the implicit flow. " + - "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + - "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + - "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); + throw new InvalidOperationException(new StringBuilder() + .AppendLine("At least one asymmetric signing key must be registered when enabling the implicit flow.") + .Append("Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' ") + .Append("or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call ") + .Append("'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.") + .ToString()); } // Automatically add the offline_access scope if the refresh token grant has been enabled. diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs index 9e716257..c03d9703 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs @@ -295,8 +295,7 @@ namespace OpenIddict.Server context.Request.SetProperty(OpenIddictConstants.Properties.AuthenticationTicket, context.Ticket); } - if (options.DisableTokenRevocation || (!context.Request.IsAuthorizationCodeGrantType() && - !context.Request.IsRefreshTokenGrantType())) + if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) { // Invoke the rest of the pipeline to allow // the user code to handle the token request. @@ -309,51 +308,85 @@ namespace OpenIddict.Server Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); - // Extract the token identifier from the authentication ticket. - var identifier = context.Ticket.GetTokenId(); - Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); + // Unless token revocation was explicitly disabled, ensure + // the authorization code/refresh token is still valid. + if (!options.DisableTokenStorage) + { + // Extract the token identifier from the authentication ticket. + var identifier = context.Ticket.GetTokenId(); + Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); + + // Retrieve the authorization code/refresh token from the request properties. + var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); + Debug.Assert(token != null, "The token shouldn't be null."); + + // If the authorization code/refresh token is already marked as redeemed, this may indicate that + // it was compromised. In this case, revoke the authorization and all the associated tokens. + // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. + if (await _tokenManager.IsRedeemedAsync(token)) + { + await TryRevokeTokenAsync(token); - // Retrieve the authorization code/refresh token from the request properties. - var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); - Debug.Assert(token != null, "The token shouldn't be null."); + // Try to revoke the authorization and the associated tokens. + // If the operation fails, the helpers will automatically log + // and swallow the exception to ensure that a valid error + // response will be returned to the client application. + if (!options.DisableAuthorizationStorage) + { + await TryRevokeAuthorizationAsync(context.Ticket); + await TryRevokeTokensAsync(context.Ticket); + } - // If the authorization code/refresh token is already marked as redeemed, this may indicate that - // it was compromised. In this case, revoke the authorization and all the associated tokens. - // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. - if (await _tokenManager.IsRedeemedAsync(token)) - { - // Try to revoke the authorization and the associated tokens. - // If the operation fails, the helpers will automatically log - // and swallow the exception to ensure that a valid error - // response will be returned to the client application. - await TryRevokeAuthorizationAsync(context.Ticket); - await TryRevokeTokensAsync(context.Ticket); - await TryRevokeTokenAsync(token); + _logger.LogError("The token request was rejected because the authorization code " + + "or refresh token '{Identifier}' has already been redeemed.", identifier); - _logger.LogError("The token request was rejected because the authorization code " + - "or refresh token '{Identifier}' has already been redeemed.", identifier); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code has already been redeemed." : + "The specified refresh token has already been redeemed."); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The specified authorization code has already been redeemed." : - "The specified refresh token has already been redeemed."); + return; + } - return; + else if (!await _tokenManager.IsValidAsync(token)) + { + _logger.LogError("The token request was rejected because the authorization code " + + "or refresh token '{Identifier}' was no longer valid.", identifier); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code is no longer valid." : + "The specified refresh token is no longer valid."); + + return; + } } - else if (!await _tokenManager.IsValidAsync(token)) + // Unless authorization revocation was explicitly disabled, ensure the + // authorization associated with the code/refresh token is still valid. + if (!options.DisableAuthorizationStorage) { - _logger.LogError("The token request was rejected because the authorization code " + - "or refresh token '{Identifier}' was no longer valid.", identifier); + // Extract the authorization identifier from the authentication ticket. + var identifier = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); + if (!string.IsNullOrEmpty(identifier)) + { + var authorization = await _authorizationManager.FindByIdAsync(identifier); + if (authorization == null || !await _authorizationManager.IsValidAsync(authorization)) + { + _logger.LogError("The token '{Identifier}' was rejected because " + + "the associated authorization was no longer valid."); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The specified authorization code is no longer valid." : - "The specified refresh token is no longer valid."); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The authorization associated with the authorization code is no longer valid." : + "The authorization associated with the refresh token is no longer valid."); - return; + return; + } + } } // Invoke the rest of the pipeline to allow diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs index 98ef4850..fa2594a1 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs @@ -83,8 +83,8 @@ namespace OpenIddict.Server [NotNull] OpenIdConnectRequest request, [NotNull] ISecureDataFormat format) { - Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), - "Token revocation cannot be disabled when using reference tokens."); + Debug.Assert(!(options.DisableTokenStorage && options.UseReferenceTokens), + "Token storage cannot be disabled when using reference tokens."); Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || @@ -103,7 +103,7 @@ namespace OpenIddict.Server ticket.Properties.ExpiresUtc = properties.ExpiresUtc; } - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return null; } @@ -216,7 +216,7 @@ namespace OpenIddict.Server [NotNull] OpenIdConnectRequest request, [NotNull] ISecureDataFormat format) { - Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), + Debug.Assert(!(options.DisableTokenStorage && options.UseReferenceTokens), "Token revocation cannot be disabled when using reference tokens."); Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs index 825e5798..ae0c96fc 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs @@ -138,24 +138,40 @@ namespace OpenIddict.Server return; } - // If the received token is not a reference access token, - // skip the additional reference token validation checks. - if (!options.UseReferenceTokens) + // If an authorization was attached to the access token, ensure it is still valid. + if (!options.DisableAuthorizationStorage && + context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId)) { - return; - } + var authorization = await _authorizationManager.FindByIdAsync( + context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId)); + + if (authorization == null || !await _authorizationManager.IsValidAsync(authorization)) + { + _logger.LogError("The token '{Identifier}' was declared as inactive because " + + "the associated authorization was no longer valid.", identifier); + + context.Active = false; - // Retrieve the token from the request properties. If it's marked as invalid, return active = false. - var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); - Debug.Assert(token != null, "The token shouldn't be null."); + return; + } + } - if (!await _tokenManager.IsValidAsync(token)) + // If the received token is a reference access token - i.e a token for + // which an entry exists in the database - ensure it is still valid. + if (options.UseReferenceTokens) { - _logger.LogInformation("The token '{Identifier}' was declared as inactive because it was revoked.", identifier); + // Retrieve the token from the request properties. If it's marked as invalid, return active = false. + var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); + Debug.Assert(token != null, "The token shouldn't be null."); - context.Active = false; + if (!await _tokenManager.IsValidAsync(token)) + { + _logger.LogInformation("The token '{Identifier}' was declared as inactive because it was revoked.", identifier); - return; + context.Active = false; + + return; + } } await base.HandleIntrospectionRequest(context); diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs index ecade3ac..b6dbbfab 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs @@ -22,7 +22,7 @@ namespace OpenIddict.Server { var options = (OpenIddictServerOptions) context.Options; - Debug.Assert(!options.DisableTokenRevocation, "Token revocation support shouldn't be disabled at this stage."); + Debug.Assert(!options.DisableTokenStorage, "Token storage 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)) diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs index 17abcff3..838eb32f 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs @@ -17,7 +17,7 @@ namespace OpenIddict.Server public override async Task DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context) { var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return; } @@ -40,7 +40,7 @@ namespace OpenIddict.Server public override async Task DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context) { var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return; } @@ -59,7 +59,7 @@ namespace OpenIddict.Server public override async Task DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context) { var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return; } @@ -78,7 +78,7 @@ namespace OpenIddict.Server public override async Task SerializeAccessToken([NotNull] SerializeAccessTokenContext context) { var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return; } @@ -104,7 +104,7 @@ namespace OpenIddict.Server public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) { var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return; } @@ -132,7 +132,7 @@ namespace OpenIddict.Server public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return; } diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs index 7af7da20..868dfcb9 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs @@ -117,56 +117,51 @@ namespace OpenIddict.Server context.IncludeRefreshToken &= options.UseRollingTokens; } - // If token revocation was explicitly disabled, - // none of the following security routines apply. - if (options.DisableTokenRevocation) + // If token revocation was explicitly disabled, none of the following security routines apply. + if (!options.DisableTokenStorage) { - await base.ProcessSigninResponse(context); - - return; - } - - var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{context.Ticket.GetTokenId()}"); - Debug.Assert(token != null, "The token shouldn't be null."); - - // If rolling tokens are enabled or if the request is a grant_type=authorization_code request, - // mark the authorization code or the refresh token as redeemed to prevent future reuses. - // If the operation fails, return an error indicating the code/token is no longer valid. - // See https://tools.ietf.org/html/rfc6749#section-6 for more information. - if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType()) - { - if (!await TryRedeemTokenAsync(token)) + var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{context.Ticket.GetTokenId()}"); + Debug.Assert(token != null, "The token shouldn't be null."); + + // If rolling tokens are enabled or if the request is a grant_type=authorization_code request, + // mark the authorization code or the refresh token as redeemed to prevent future reuses. + // If the operation fails, return an error indicating the code/token is no longer valid. + // See https://tools.ietf.org/html/rfc6749#section-6 for more information. + if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType()) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The specified authorization code is no longer valid." : - "The specified refresh token is no longer valid."); + if (!await TryRedeemTokenAsync(token)) + { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code is no longer valid." : + "The specified refresh token is no longer valid."); - return; + return; + } } - } - if (context.Request.IsRefreshTokenGrantType()) - { - // When rolling tokens are enabled, try to revoke all the previously issued tokens - // associated with the authorization if the request is a refresh_token request. - // If the operation fails, silently ignore the error and keep processing the request: - // this may indicate that one of the revoked tokens was modified by a concurrent request. - if (options.UseRollingTokens) + if (context.Request.IsRefreshTokenGrantType()) { - await TryRevokeTokensAsync(context.Ticket); - } + // When rolling tokens are enabled, try to revoke all the previously issued tokens + // associated with the authorization if the request is a refresh_token request. + // If the operation fails, silently ignore the error and keep processing the request: + // this may indicate that one of the revoked tokens was modified by a concurrent request. + if (options.UseRollingTokens) + { + await TryRevokeTokensAsync(context.Ticket); + } - // When rolling tokens are disabled, try to extend the expiration date - // of the existing token instead of returning a new refresh token - // with a new expiration date if sliding expiration was not disabled. - // If the operation fails, silently ignore the error and keep processing - // the request: this may indicate that a concurrent refresh token request - // already updated the expiration date associated with the refresh token. - if (!options.UseRollingTokens && options.UseSlidingExpiration) - { - await TryExtendTokenAsync(token, context.Ticket, options); + // When rolling tokens are disabled, try to extend the expiration date + // of the existing token instead of returning a new refresh token + // with a new expiration date if sliding expiration was not disabled. + // If the operation fails, silently ignore the error and keep processing + // the request: this may indicate that a concurrent refresh token request + // already updated the expiration date associated with the refresh token. + if (!options.UseRollingTokens && options.UseSlidingExpiration) + { + await TryExtendTokenAsync(token, context.Ticket, options); + } } } } @@ -174,7 +169,8 @@ namespace OpenIddict.Server // If no authorization was explicitly attached to the authentication ticket, // create an ad hoc authorization if an authorization code or a refresh token // is going to be returned to the client application as part of the response. - if (!context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId) && + if (!options.DisableAuthorizationStorage && + !context.Ticket.HasProperty(OpenIddictConstants.Properties.AuthorizationId) && (context.IncludeAuthorizationCode || context.IncludeRefreshToken)) { await CreateAuthorizationAsync(context.Ticket, options, context.Request); diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 0d3e381a..fb7b4a93 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -341,6 +341,16 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerBuilder AllowRefreshTokenFlow() => Configure(options => options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.RefreshToken)); + /// + /// Disables authorization storage so that ad-hoc authorizations are + /// not created when an authorization code or refresh token is issued + /// and can't be revoked to prevent associated tokens from being used. + /// Using this option is generally NOT recommended. + /// + /// The . + public OpenIddictServerBuilder DisableAuthorizationStorage() + => Configure(options => options.DisableAuthorizationStorage = true); + /// /// Disables the configuration endpoint. /// @@ -364,7 +374,7 @@ namespace Microsoft.Extensions.DependencyInjection /// /// Disables sliding expiration. When using this option, refresh tokens - /// are issued with a fixed expiration date: when it expires, a complete + /// are issued with a fixed expiration date: when they expire, a complete /// authorization flow must be started to retrieve a new refresh token. /// /// The . @@ -372,13 +382,13 @@ namespace Microsoft.Extensions.DependencyInjection => Configure(options => options.UseSlidingExpiration = false); /// - /// Disables token revocation, so that authorization code and + /// Disables token storage, so that authorization code and /// refresh tokens are never stored and cannot be revoked. - /// Using this option is generally not recommended. + /// Using this option is generally NOT recommended. /// /// The . - public OpenIddictServerBuilder DisableTokenRevocation() - => Configure(options => options.DisableTokenRevocation = true); + public OpenIddictServerBuilder DisableTokenStorage() + => Configure(options => options.DisableTokenStorage = true); /// /// Enables the authorization endpoint. diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index edfddd9f..28265df7 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -37,11 +37,10 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddScoped(); builder.Services.TryAddScoped(provider => { - InvalidOperationException CreateException() - => new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling the server handler.") - .Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.") - .ToString()); + InvalidOperationException CreateException() => new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the server handler.") + .Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.") + .ToString()); return new OpenIddictServerProvider( provider.GetRequiredService>(), diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 784b2f73..d29c6c21 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -61,11 +61,18 @@ namespace OpenIddict.Server }; /// - /// Gets or sets a boolean indicating whether token revocation should be disabled. + /// Gets or sets a boolean indicating whether authorization storage should be disabled. + /// When disabled, ad-hoc authorizations are not created when an authorization code or + /// refresh token is issued and can't be revoked to prevent associated tokens from being used. + /// + public bool DisableAuthorizationStorage { get; set; } + + /// + /// Gets or sets a boolean indicating whether token storage 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; } + public bool DisableTokenStorage { get; set; } /// /// Gets or sets a boolean indicating whether request caching should be enabled. diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs index 680cf32f..33f9e0bf 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs @@ -5,6 +5,7 @@ */ using System; +using System.Text; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Client; using AspNet.Security.OpenIdConnect.Primitives; @@ -127,8 +128,7 @@ namespace OpenIddict.Server.Tests return client.GetAsync("/"); }); - Assert.Equal("The authorization endpoint must be enabled to use " + - "the authorization code and implicit flows.", exception.Message); + Assert.Equal("The authorization endpoint must be enabled to use the authorization code and implicit flows.", exception.Message); } [Theory] @@ -159,7 +159,7 @@ namespace OpenIddict.Server.Tests } [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenTokenRevocationIsDisabled() + public async Task PostConfigure_ThrowsAnExceptionWhenTokenStorageIsDisabled() { // Arrange var server = CreateAuthorizationServer(builder => @@ -167,7 +167,7 @@ namespace OpenIddict.Server.Tests builder.EnableAuthorizationEndpoint("/connect/authorize") .EnableRevocationEndpoint("/connect/revocation") .AllowImplicitFlow() - .DisableTokenRevocation(); + .DisableTokenStorage(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -178,18 +178,18 @@ namespace OpenIddict.Server.Tests return client.GetAsync("/"); }); - Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message); + Assert.Equal("The revocation endpoint cannot be enabled when token storage is disabled.", exception.Message); } [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenRevocationDisabled() + public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenStorageDisabled() { // Arrange var server = CreateAuthorizationServer(builder => { builder.EnableAuthorizationEndpoint("/connect/authorize") .AllowImplicitFlow() - .DisableTokenRevocation() + .DisableTokenStorage() .UseReferenceTokens(); }); @@ -201,18 +201,18 @@ namespace OpenIddict.Server.Tests return client.GetAsync("/"); }); - Assert.Equal("Reference tokens cannot be used when disabling token revocation.", exception.Message); + Assert.Equal("Reference tokens cannot be used when disabling token storage.", exception.Message); } [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenRevocationDisabled() + public async Task PostConfigure_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenStorageDisabled() { // Arrange var server = CreateAuthorizationServer(builder => { builder.EnableAuthorizationEndpoint("/connect/authorize") .AllowImplicitFlow() - .DisableTokenRevocation(); + .DisableTokenStorage(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -224,7 +224,7 @@ namespace OpenIddict.Server.Tests }); Assert.Equal("Sliding expiration must be disabled when turning off " + - "token revocation if rolling tokens are not used.", exception.Message); + "token storage if rolling tokens are not used.", exception.Message); } [Fact] @@ -270,11 +270,12 @@ namespace OpenIddict.Server.Tests return client.GetAsync("/"); }); - Assert.Equal( - "At least one signing key must be registered when using JWT as the access token format. " + - "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + - "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + - "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); + Assert.Equal(new StringBuilder() + .AppendLine("At least one signing key must be registered when using JWT as the access token format.") + .Append("Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' ") + .Append("or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call ") + .Append("'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.") + .ToString(), exception.Message); } [Fact] @@ -295,11 +296,12 @@ namespace OpenIddict.Server.Tests return client.GetAsync("/"); }); - Assert.Equal( - "At least one asymmetric signing key must be registered when enabling the implicit flow. " + - "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + - "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + - "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); + Assert.Equal(new StringBuilder() + .AppendLine("At least one asymmetric signing key must be registered when enabling the implicit flow.") + .Append("Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' ") + .Append("or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call ") + .Append("'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.") + .ToString(), exception.Message); } private static TestServer CreateAuthorizationServer(Action configuration = null) diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs index db08b3fa..a43804c7 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs @@ -736,7 +736,7 @@ namespace OpenIddict.Server.Tests } [Fact] - public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenRevocationIsDisabled() + public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenStorageIsDisabled() { // Arrange var ticket = new AuthenticationTicket( @@ -777,7 +777,7 @@ namespace OpenIddict.Server.Tests builder.Configure(options => options.AuthorizationCodeFormat = format.Object); builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - builder.DisableTokenRevocation(); + builder.DisableTokenStorage(); builder.DisableSlidingExpiration(); }); @@ -797,7 +797,7 @@ namespace OpenIddict.Server.Tests } [Fact] - public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenRevocationIsDisabled() + public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenStorageIsDisabled() { // Arrange var ticket = new AuthenticationTicket( @@ -837,7 +837,7 @@ namespace OpenIddict.Server.Tests builder.Configure(options => options.RefreshTokenFormat = format.Object); builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - builder.DisableTokenRevocation(); + builder.DisableTokenStorage(); builder.DisableSlidingExpiration(); }); @@ -1682,6 +1682,546 @@ namespace OpenIddict.Server.Tests Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); } + [Fact] + public async Task HandleTokenRequest_AuthorizationAssociatedWithCodeIsIgnoredWhenAuthorizationStorageIsDisabled() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.DisableAuthorizationStorage(); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.NotNull(response.AccessToken); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task HandleTokenRequest_AuthorizationAssociatedWithRefreshTokenIsIgnoredWhenAuthorizationStorageIsDisabled() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.DisableAuthorizationStorage(); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.NotNull(response.AccessToken); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeCannotBeFound() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(value: null); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The authorization associated with the authorization code is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithAuthorizationCodeIsInvalid() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetPresenters("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) + .Returns(ticket); + + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + + instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) + .ReturnsAsync(false); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AuthorizationCodeFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + Code = "SplxlOBeZQQYbYS6WxSbIA", + GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The authorization associated with the authorization code is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithRefreshTokenCannotBeFound() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(value: null); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56")); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The authorization associated with the refresh token is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleTokenRequest_RequestIsRejectedWhenAuthorizationAssociatedWithRefreshTokenIsInvalid() + { + // Arrange + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); + ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + + instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) + .ReturnsAsync(false); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56")); + + instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIdConnectConstants.GrantTypes.RefreshToken, + RefreshToken = "8xLOxBtZp8" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The authorization associated with the refresh token is no longer valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); + } + [Theory] [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] @@ -1730,6 +2270,9 @@ namespace OpenIddict.Server.Tests instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) .ReturnsAsync(true); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); }); var server = CreateAuthorizationServer(builder => @@ -1772,6 +2315,17 @@ namespace OpenIddict.Server.Tests .ReturnsAsync(true); })); + builder.Services.AddSingleton(CreateAuthorizationManager(instance => + { + var authorization = new OpenIddictAuthorization(); + + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + + instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) + .ReturnsAsync(true); + })); + builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); builder.Services.AddSingleton(manager); diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs index ec46e80b..52b6663e 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs @@ -441,6 +441,295 @@ namespace OpenIddict.Server.Tests Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny()), Times.Once()); } + [Fact] + public async Task HandleIntrospectionRequest_AuthorizationIsIgnoredWhenAuthorizationStorageIsDisabled() + { + // Arrange + var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetAudiences("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.DisableAuthorizationStorage(); + builder.UseReferenceTokens(); + + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI" + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Never()); + } + + [Fact] + public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationCannotBeFound() + { + // Arrange + var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetAudiences("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(value: null); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AccessTokenFormat = format.Object); + + builder.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI" + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task HandleIntrospectionRequest_RequestIsRejectedWhenAuthorizationIsInvalid() + { + // Arrange + var identity = new ClaimsIdentity(OpenIddictServerDefaults.AuthenticationScheme); + identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIddictServerDefaults.AuthenticationScheme); + + ticket.SetAudiences("Fabrikam"); + ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA")) + .Returns(ticket); + + var authorization = new OpenIddictAuthorization(); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + + instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) + .ReturnsAsync(false); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + var token = new OpenIddictToken(); + + instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + .Returns(new ValueTask("2YotnFZFEjr1zCsicMWpAA")); + + instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(false); + + instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny())) + .Returns(new ValueTask("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0")); + })); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.AccessTokenFormat = format.Object); + + builder.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI" + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny()), Times.Once()); + } + [Fact] public async Task HandleIntrospectionRequest_RequestIsRejectedWhenReferenceTokenIsInvalid() { diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs index 02868155..223bfa94 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs @@ -1791,7 +1791,7 @@ namespace OpenIddict.Server.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - builder.DisableTokenRevocation(); + builder.DisableTokenStorage(); builder.DisableSlidingExpiration(); }); @@ -2195,7 +2195,7 @@ namespace OpenIddict.Server.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - builder.DisableTokenRevocation(); + builder.DisableTokenStorage(); builder.DisableSlidingExpiration(); }); diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs index 429081a1..18009f25 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs @@ -765,7 +765,6 @@ namespace OpenIddict.Server.Tests ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); - ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"); var format = new Mock>(); @@ -809,6 +808,17 @@ namespace OpenIddict.Server.Tests var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateAuthorizationManager(instance => + { + var authorization = new OpenIddictAuthorization(); + + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + + instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) + .ReturnsAsync(true); + })); + builder.Services.AddSingleton(manager); builder.UseRollingTokens(); @@ -887,6 +897,17 @@ namespace OpenIddict.Server.Tests var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateAuthorizationManager(instance => + { + var authorization = new OpenIddictAuthorization(); + + instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny())) + .ReturnsAsync(authorization); + + instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny())) + .ReturnsAsync(true); + })); + builder.Services.AddSingleton(manager); builder.Configure(options => options.RefreshTokenFormat = format.Object); @@ -902,6 +923,7 @@ namespace OpenIddict.Server.Tests }); // Assert + Assert.NotNull(response.AccessToken); Assert.Null(response.RefreshToken); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); @@ -1188,6 +1210,75 @@ namespace OpenIddict.Server.Tests It.IsAny()), Times.Once()); } + [Fact] + public async Task ProcessSigninResponse_AdHocAuthorizationIsNotCreatedWhenAuthorizationStorageIsDisabled() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateAuthorizationManager(instance => + { + instance.Setup(mock => mock.FindByIdAsync("1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70", It.IsAny())) + .ReturnsAsync(new OpenIddictAuthorization()); + }); + + 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.HasPermissionAsync(application, + OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.HasPermissionAsync(application, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Public)); + + instance.Setup(mock => mock.GetIdAsync(application, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + })); + + builder.Services.AddSingleton(CreateTokenManager(instance => + { + instance.Setup(mock => mock.CreateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); + })); + + builder.Services.AddSingleton(manager); + + builder.DisableAuthorizationStorage(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code, + }); + + // Assert + Assert.NotNull(response.Code); + + Mock.Get(manager).Verify(mock => mock.CreateAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + [Fact] public async Task ProcessSigninResponse_CustomPublicParametersAreAddedToAuthorizationResponse() { @@ -1351,10 +1442,10 @@ namespace OpenIddict.Server.Tests .SetDefaultScopeEntity() .SetDefaultTokenEntity(); - options.Services.AddSingleton(CreateApplicationManager()); - options.Services.AddSingleton(CreateAuthorizationManager()); - options.Services.AddSingleton(CreateScopeManager()); - options.Services.AddSingleton(CreateTokenManager()); + options.Services.AddSingleton(CreateApplicationManager()) + .AddSingleton(CreateAuthorizationManager()) + .AddSingleton(CreateScopeManager()) + .AddSingleton(CreateTokenManager()); }) .AddServer(options => diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index 760d6c24..ac2c5441 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -296,6 +296,22 @@ namespace OpenIddict.Server.Tests Assert.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken, options.GrantTypes); } + [Fact] + public void DisableAuthorizationStorage_AuthorizationStorageIsDisabled() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.DisableAuthorizationStorage(); + + var options = GetOptions(services); + + // Assert + Assert.True(options.DisableAuthorizationStorage); + } + [Fact] public void DisableConfigurationEndpoint_ConfigurationEndpointIsDisabled() { @@ -345,19 +361,19 @@ namespace OpenIddict.Server.Tests } [Fact] - public void DisableTokenRevocation_TokenRevocationIsDisabled() + public void DisableTokenStorage_TokenStorageIsDisabled() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); // Act - builder.DisableTokenRevocation(); + builder.DisableTokenStorage(); var options = GetOptions(services); // Assert - Assert.True(options.DisableTokenRevocation); + Assert.True(options.DisableTokenStorage); } [Fact]