diff --git a/OpenIddict.sln b/OpenIddict.sln index d58b859f..fc61b4c9 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -10,7 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{19E887E7 build\key.snk = build\key.snk build\packages.props = build\packages.props build\repo.props = build\repo.props - build\tests.props = build\tests.props build\version.props = build\version.props EndProjectSection EndProject @@ -56,9 +55,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkCore.Models", "src\OpenIddict.EntityFrameworkCore.Models\OpenIddict.EntityFrameworkCore.Models.csproj", "{B5371534-4C33-41FA-B3D3-7D70D632DB15}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.MongoDb", "src\OpenIddict.MongoDb\OpenIddict.MongoDb.csproj", "{BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.MongoDb", "src\OpenIddict.MongoDb\OpenIddict.MongoDb.csproj", "{BACF1DD4-8390-48D4-BD9B-DA1EC00C1F98}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.MongoDb.Models", "src\OpenIddict.MongoDb.Models\OpenIddict.MongoDb.Models.csproj", "{14C55FB6-9626-4BDE-8961-3BE91DDD6418}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.MongoDb.Models", "src\OpenIddict.MongoDb.Models\OpenIddict.MongoDb.Models.csproj", "{14C55FB6-9626-4BDE-8961-3BE91DDD6418}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/korebuild-lock.txt b/korebuild-lock.txt new file mode 100644 index 00000000..3673744d --- /dev/null +++ b/korebuild-lock.txt @@ -0,0 +1,2 @@ +version:2.1.0-rtm-15783 +commithash:5fc2b2f607f542a2ffde11c19825e786fc1a3774 diff --git a/korebuild.json b/korebuild.json new file mode 100644 index 00000000..678d8bb9 --- /dev/null +++ b/korebuild.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.1/tools/korebuild.schema.json", + "channel": "release/2.1" +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs index 85c181e4..be35f09c 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs @@ -295,6 +295,7 @@ namespace OpenIddict.Server var options = (OpenIddictServerOptions) context.Options; var logger = GetLogger(context.HttpContext.RequestServices); + var authorizationManager = GetAuthorizationManager(context.HttpContext.RequestServices); var tokenManager = GetTokenManager(context.HttpContext.RequestServices); if (context.Ticket != null) @@ -303,8 +304,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. @@ -317,51 +317,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, context.HttpContext); - // 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, context.HttpContext); + await TryRevokeTokensAsync(context.Ticket, context.HttpContext); + } - // 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, context.HttpContext); - await TryRevokeTokensAsync(context.Ticket, context.HttpContext); - await TryRevokeTokenAsync(token, context.HttpContext); + 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 e5458fe0..8182a42a 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs @@ -92,8 +92,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 || @@ -116,7 +116,7 @@ namespace OpenIddict.Server ticket.Properties.ExpiresUtc = properties.ExpiresUtc; } - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return null; } @@ -232,6 +232,9 @@ namespace OpenIddict.Server var logger = GetLogger(context.RequestServices); var tokenManager = GetTokenManager(context.RequestServices); + Debug.Assert(!(options.DisableTokenStorage && options.UseReferenceTokens), + "Token revocation cannot be disabled when using reference tokens."); + Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || type == OpenIdConnectConstants.TokenUsages.RefreshToken, diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs index 77c90b4e..9710540d 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs @@ -103,6 +103,7 @@ namespace OpenIddict.Server var options = (OpenIddictServerOptions) context.Options; var logger = GetLogger(context.HttpContext.RequestServices); + var authorizationManager = GetAuthorizationManager(context.HttpContext.RequestServices); var tokenManager = GetTokenManager(context.HttpContext.RequestServices); Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); @@ -145,24 +146,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 ecbd6bcd..c90c4336 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs @@ -26,7 +26,7 @@ namespace OpenIddict.Server var logger = GetLogger(context.HttpContext.RequestServices); var applicationManager = GetApplicationManager(context.HttpContext.RequestServices); - 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 cc5c7998..b7484786 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; } @@ -105,7 +105,7 @@ namespace OpenIddict.Server public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) { var options = (OpenIddictServerOptions) context.Options; - if (options.DisableTokenRevocation) + if (options.DisableTokenStorage) { return; } @@ -134,7 +134,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 71cc39fb..96d35517 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs @@ -93,56 +93,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, context.HttpContext)) + 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.HttpContext)) + { + 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, context.HttpContext); - } + // 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, context.HttpContext); + } - // 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, context.HttpContext, 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, context.HttpContext, options); + } } } } @@ -150,7 +145,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.HttpContext, context.Request); diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 631079e5..3209c9fc 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -302,6 +302,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. /// @@ -325,7 +335,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 . @@ -333,13 +343,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 3d0ec369..3cfdf6db 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -6,6 +6,7 @@ using System; using System.Linq; +using System.Text; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; @@ -156,40 +157,51 @@ namespace Microsoft.Extensions.DependencyInjection 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."); + } + + if (options.UseSlidingExpiration && options.DisableTokenStorage && !options.UseRollingTokens) { throw new InvalidOperationException( - "Reference tokens cannot be used when configuring JWT as the access token format."); + "Sliding expiration must be disabled when turning off token storage if rolling tokens are not used."); } - if (options.UseSlidingExpiration && options.DisableTokenRevocation && !options.UseRollingTokens) + if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0) { - throw new InvalidOperationException("Sliding expiration must be disabled when turning off " + - "token revocation if rolling tokens are not used."); + 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 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/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 30b6942d..ab5f5127 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -52,11 +52,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/OpenIddictServerProviderTests.Exchange.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs index d6d1ac01..be93e915 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs @@ -738,7 +738,7 @@ namespace OpenIddict.Server.Tests } [Fact] - public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenRevocationIsDisabled() + public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenStorageIsDisabled() { // Arrange var ticket = new AuthenticationTicket( @@ -779,7 +779,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(); }); @@ -799,7 +799,7 @@ namespace OpenIddict.Server.Tests } [Fact] - public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenRevocationIsDisabled() + public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenStorageIsDisabled() { // Arrange var ticket = new AuthenticationTicket( @@ -839,7 +839,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(); }); @@ -1684,6 +1684,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)] @@ -1732,6 +2272,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 => @@ -1774,6 +2317,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 fa02fccc..38e1408d 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs @@ -442,6 +442,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 93570012..96ec8490 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs @@ -1793,7 +1793,7 @@ namespace OpenIddict.Server.Tests builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); - builder.DisableTokenRevocation(); + builder.DisableTokenStorage(); builder.DisableSlidingExpiration(); }); @@ -2197,7 +2197,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 d5456711..88bef8a0 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs @@ -766,7 +766,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>(); @@ -810,6 +809,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(); @@ -888,6 +898,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); @@ -903,6 +924,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()); @@ -1189,6 +1211,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() { @@ -1352,10 +1443,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 28d64c5d..a82c2e14 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -33,11 +33,10 @@ namespace OpenIddict.Server.Tests // Act builder.Configure(configuration => configuration.Description.DisplayName = "OpenIddict"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("OpenIddict", options.Value.Description.DisplayName); + Assert.Equal("OpenIddict", options.Description.DisplayName); } [Fact] @@ -52,11 +51,10 @@ namespace OpenIddict.Server.Tests // Act builder.AddEphemeralSigningKey(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(1, options.Value.SigningCredentials.Count); + Assert.Equal(1, options.SigningCredentials.Count); } [Theory] @@ -79,9 +77,8 @@ namespace OpenIddict.Server.Tests // Act builder.AddEphemeralSigningKey(algorithm); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); - var credentials = options.Value.SigningCredentials[0]; + var options = GetOptions(services); + var credentials = options.SigningCredentials[0]; // Assert Assert.Equal(algorithm, credentials.Algorithm); @@ -111,11 +108,10 @@ namespace OpenIddict.Server.Tests // Act builder.AddSigningKey(key); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Same(key, options.Value.SigningCredentials[0].Key); + Assert.Same(key, options.SigningCredentials[0].Key); } [Fact] @@ -133,11 +129,10 @@ namespace OpenIddict.Server.Tests resource: "OpenIddict.Server.Tests.Certificate.pfx", password: "OpenIddict"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.IsType(typeof(X509SecurityKey), options.Value.SigningCredentials[0].Key); + Assert.IsType(typeof(X509SecurityKey), options.SigningCredentials[0].Key); } [Fact] @@ -152,11 +147,10 @@ namespace OpenIddict.Server.Tests // Act builder.AllowAuthorizationCodeFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode, options.GrantTypes); } [Fact] @@ -171,11 +165,10 @@ namespace OpenIddict.Server.Tests // Act builder.AllowClientCredentialsFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials, options.GrantTypes); } [Fact] @@ -190,11 +183,10 @@ namespace OpenIddict.Server.Tests // Act builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.Value.GrantTypes); + Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.GrantTypes); } [Fact] @@ -209,11 +201,10 @@ namespace OpenIddict.Server.Tests // Act builder.AllowImplicitFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.Implicit, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.Implicit, options.GrantTypes); } [Fact] @@ -228,11 +219,10 @@ namespace OpenIddict.Server.Tests // Act builder.AllowPasswordFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.Password, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.Password, options.GrantTypes); } [Fact] @@ -247,11 +237,26 @@ namespace OpenIddict.Server.Tests // Act builder.AllowRefreshTokenFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken, options.Value.GrantTypes); + 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] @@ -266,11 +271,10 @@ namespace OpenIddict.Server.Tests // Act builder.DisableConfigurationEndpoint(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(PathString.Empty, options.Value.ConfigurationEndpointPath); + Assert.Equal(PathString.Empty, options.ConfigurationEndpointPath); } [Fact] @@ -285,11 +289,10 @@ namespace OpenIddict.Server.Tests // Act builder.DisableCryptographyEndpoint(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(PathString.Empty, options.Value.CryptographyEndpointPath); + Assert.Equal(PathString.Empty, options.CryptographyEndpointPath); } [Fact] @@ -304,15 +307,14 @@ namespace OpenIddict.Server.Tests // Act builder.DisableSlidingExpiration(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.False(options.Value.UseSlidingExpiration); + Assert.False(options.UseSlidingExpiration); } [Fact] - public void DisableTokenRevocation_TokenRevocationIsDisabled() + public void DisableTokenStorage_TokenStorageIsDisabled() { // Arrange var services = new ServiceCollection(); @@ -321,13 +323,12 @@ namespace OpenIddict.Server.Tests var builder = CreateBuilder(services); // Act - builder.DisableTokenRevocation(); + builder.DisableTokenStorage(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.True(options.Value.DisableTokenRevocation); + Assert.True(options.DisableTokenStorage); } [Fact] @@ -342,11 +343,10 @@ namespace OpenIddict.Server.Tests // Act builder.EnableAuthorizationEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.AuthorizationEndpointPath); + Assert.Equal("/endpoint-path", options.AuthorizationEndpointPath); } [Fact] @@ -361,11 +361,10 @@ namespace OpenIddict.Server.Tests // Act builder.EnableIntrospectionEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.IntrospectionEndpointPath); + Assert.Equal("/endpoint-path", options.IntrospectionEndpointPath); } [Fact] @@ -380,11 +379,10 @@ namespace OpenIddict.Server.Tests // Act builder.EnableLogoutEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.LogoutEndpointPath); + Assert.Equal("/endpoint-path", options.LogoutEndpointPath); } [Fact] @@ -399,11 +397,10 @@ namespace OpenIddict.Server.Tests // Act builder.EnableRequestCaching(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.True(options.Value.EnableRequestCaching); + Assert.True(options.EnableRequestCaching); } [Fact] @@ -418,11 +415,10 @@ namespace OpenIddict.Server.Tests // Act builder.EnableRevocationEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.RevocationEndpointPath); + Assert.Equal("/endpoint-path", options.RevocationEndpointPath); } [Fact] @@ -437,11 +433,10 @@ namespace OpenIddict.Server.Tests // Act builder.EnableScopeValidation(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.True(options.Value.EnableScopeValidation); + Assert.True(options.EnableScopeValidation); } [Fact] @@ -456,11 +451,10 @@ namespace OpenIddict.Server.Tests // Act builder.EnableTokenEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.TokenEndpointPath); + Assert.Equal("/endpoint-path", options.TokenEndpointPath); } [Fact] @@ -475,11 +469,10 @@ namespace OpenIddict.Server.Tests // Act builder.EnableUserinfoEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.UserinfoEndpointPath); + Assert.Equal("/endpoint-path", options.UserinfoEndpointPath); } [Fact] @@ -494,11 +487,10 @@ namespace OpenIddict.Server.Tests // Act builder.RequireClientIdentification(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.True(options.Value.RequireClientIdentification); + Assert.True(options.RequireClientIdentification); } [Fact] @@ -513,11 +505,10 @@ namespace OpenIddict.Server.Tests // Act builder.SetAccessTokenLifetime(TimeSpan.FromMinutes(42)); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.Value.AccessTokenLifetime); + Assert.Equal(TimeSpan.FromMinutes(42), options.AccessTokenLifetime); } [Fact] @@ -532,11 +523,10 @@ namespace OpenIddict.Server.Tests // Act builder.SetAuthorizationCodeLifetime(TimeSpan.FromMinutes(42)); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.Value.AuthorizationCodeLifetime); + Assert.Equal(TimeSpan.FromMinutes(42), options.AuthorizationCodeLifetime); } [Fact] @@ -551,11 +541,10 @@ namespace OpenIddict.Server.Tests // Act builder.SetIdentityTokenLifetime(TimeSpan.FromMinutes(42)); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.Value.IdentityTokenLifetime); + Assert.Equal(TimeSpan.FromMinutes(42), options.IdentityTokenLifetime); } [Fact] @@ -570,11 +559,10 @@ namespace OpenIddict.Server.Tests // Act builder.SetRefreshTokenLifetime(TimeSpan.FromMinutes(42)); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.Value.RefreshTokenLifetime); + Assert.Equal(TimeSpan.FromMinutes(42), options.RefreshTokenLifetime); } [Fact] @@ -589,11 +577,10 @@ namespace OpenIddict.Server.Tests // Act builder.SetIssuer(new Uri("http://www.fabrikam.com/")); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Value.Issuer); + Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Issuer); } [Fact] @@ -624,12 +611,11 @@ namespace OpenIddict.Server.Tests // Act builder.RegisterClaims("custom_claim_1", "custom_claim_2"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains("custom_claim_1", options.Value.Claims); - Assert.Contains("custom_claim_2", options.Value.Claims); + Assert.Contains("custom_claim_1", options.Claims); + Assert.Contains("custom_claim_2", options.Claims); } [Fact] @@ -644,12 +630,11 @@ namespace OpenIddict.Server.Tests // Act builder.RegisterScopes("custom_scope_1", "custom_scope_2"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains("custom_scope_1", options.Value.Scopes); - Assert.Contains("custom_scope_2", options.Value.Scopes); + Assert.Contains("custom_scope_1", options.Scopes); + Assert.Contains("custom_scope_2", options.Scopes); } [Fact] @@ -664,11 +649,10 @@ namespace OpenIddict.Server.Tests // Act builder.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.IsType(typeof(EphemeralDataProtectionProvider), options.Value.DataProtectionProvider); + Assert.IsType(typeof(EphemeralDataProtectionProvider), options.DataProtectionProvider); } [Fact] @@ -683,11 +667,10 @@ namespace OpenIddict.Server.Tests // Act builder.UseJsonWebTokens(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.IsType(options.Value.AccessTokenHandler); + Assert.IsType(options.AccessTokenHandler); } [Fact] @@ -702,11 +685,10 @@ namespace OpenIddict.Server.Tests // Act builder.UseReferenceTokens(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.True(options.Value.UseReferenceTokens); + Assert.True(options.UseReferenceTokens); } private static IServiceCollection CreateServices() diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs index 2913edf7..bcc9653a 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs @@ -6,6 +6,7 @@ using System; using System.Reflection; +using System.Text; using AspNet.Security.OpenIdConnect.Primitives; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder.Internal; @@ -104,7 +105,7 @@ namespace OpenIddict.Server.Tests } [Fact] - public void UseOpenIddictServer_ThrowsAnExceptionWhenTokenRevocationIsDisabled() + public void UseOpenIddictServer_ThrowsAnExceptionWhenTokenStorageIsDisabled() { // Arrange var services = new ServiceCollection(); @@ -122,18 +123,18 @@ namespace OpenIddict.Server.Tests .EnableAuthorizationEndpoint("/connect/authorize") .EnableRevocationEndpoint("/connect/revocation") .AllowImplicitFlow() - .DisableTokenRevocation(); + .DisableTokenStorage(); var builder = new ApplicationBuilder(services.BuildServiceProvider()); // Act and assert var exception = Assert.Throws(() => builder.UseOpenIddictServer()); - 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 void UseOpenIddictServer_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenRevocationDisabled() + public void UseOpenIddictServer_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenStorageDisabled() { // Arrange var services = new ServiceCollection(); @@ -151,7 +152,7 @@ namespace OpenIddict.Server.Tests .AddServer() .EnableAuthorizationEndpoint("/connect/authorize") .AllowImplicitFlow() - .DisableTokenRevocation() + .DisableTokenStorage() .UseReferenceTokens(); var builder = new ApplicationBuilder(services.BuildServiceProvider()); @@ -159,7 +160,7 @@ namespace OpenIddict.Server.Tests // Act and assert var exception = Assert.Throws(() => builder.UseOpenIddictServer()); - 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] @@ -193,7 +194,7 @@ namespace OpenIddict.Server.Tests } [Fact] - public void UseOpenIddictServer_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenRevocationDisabled() + public void UseOpenIddictServer_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenStorageDisabled() { // Arrange var services = new ServiceCollection(); @@ -211,7 +212,7 @@ namespace OpenIddict.Server.Tests .AddServer() .EnableAuthorizationEndpoint("/connect/authorize") .AllowImplicitFlow() - .DisableTokenRevocation(); + .DisableTokenStorage(); var builder = new ApplicationBuilder(services.BuildServiceProvider()); @@ -219,7 +220,7 @@ namespace OpenIddict.Server.Tests var exception = Assert.Throws(() => builder.UseOpenIddictServer()); 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] @@ -246,9 +247,12 @@ namespace OpenIddict.Server.Tests // Act and assert var exception = Assert.Throws(() => builder.UseOpenIddictServer()); - 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 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); } [Fact]