Browse Source

Automatically validate the authorization associated with authorization codes/refresh tokens

pull/619/head
Kévin Chalet 8 years ago
parent
commit
aa89d0c215
  1. 48
      src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs
  2. 107
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs
  3. 8
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs
  4. 40
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs
  5. 2
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs
  6. 12
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs
  7. 84
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs
  8. 20
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  9. 9
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  10. 11
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  11. 44
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs
  12. 562
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs
  13. 289
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs
  14. 4
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs
  15. 101
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs
  16. 22
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

48
src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs

@ -7,6 +7,7 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Text;
using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server; using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -139,8 +140,7 @@ namespace OpenIddict.Server
if (!options.AuthorizationEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || if (!options.AuthorizationEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)))
{ {
throw new InvalidOperationException("The authorization endpoint must be enabled to use " + throw new InvalidOperationException("The authorization endpoint must be enabled to use the authorization code and implicit flows.");
"the authorization code and implicit flows.");
} }
// Ensure the token endpoint has been enabled when the authorization code, // 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.Password) ||
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))) options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)))
{ {
throw new InvalidOperationException("The token endpoint must be enabled to use the authorization code, " + throw new InvalidOperationException(
"client credentials, password and refresh token flows."); "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( throw new InvalidOperationException("Reference tokens cannot be used when disabling token storage.");
"Reference tokens cannot be used when disabling token revocation.");
} }
if (options.UseReferenceTokens && options.AccessTokenHandler != null) if (options.UseReferenceTokens && options.AccessTokenHandler != null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException("Reference tokens cannot be used when configuring JWT as the access token format.");
"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 " + throw new InvalidOperationException(
"token revocation if rolling tokens are not used."); "Sliding expiration must be disabled when turning off token storage if rolling tokens are not used.");
} }
if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0) if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(new StringBuilder()
"At least one signing key must be registered when using JWT as the access token format. " + .AppendLine("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()' " + .Append("Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' ")
"or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + .Append("or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call ")
"'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); .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. // 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) && if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) &&
options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))
{ {
throw new InvalidOperationException( throw new InvalidOperationException(new StringBuilder()
"At least one asymmetric signing key must be registered when enabling the implicit flow. " + .AppendLine("At least one asymmetric signing key must be registered when enabling the implicit flow.")
"Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + .Append("Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' ")
"or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + .Append("or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call ")
"'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); .Append("'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.")
.ToString());
} }
// Automatically add the offline_access scope if the refresh token grant has been enabled. // Automatically add the offline_access scope if the refresh token grant has been enabled.

107
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs

@ -295,8 +295,7 @@ namespace OpenIddict.Server
context.Request.SetProperty(OpenIddictConstants.Properties.AuthenticationTicket, context.Ticket); context.Request.SetProperty(OpenIddictConstants.Properties.AuthenticationTicket, context.Ticket);
} }
if (options.DisableTokenRevocation || (!context.Request.IsAuthorizationCodeGrantType() && if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType())
!context.Request.IsRefreshTokenGrantType()))
{ {
// Invoke the rest of the pipeline to allow // Invoke the rest of the pipeline to allow
// the user code to handle the token request. // 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."); Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
// Extract the token identifier from the authentication ticket. // Unless token revocation was explicitly disabled, ensure
var identifier = context.Ticket.GetTokenId(); // the authorization code/refresh token is still valid.
Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); 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. // Try to revoke the authorization and the associated tokens.
var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); // If the operation fails, the helpers will automatically log
Debug.Assert(token != null, "The token shouldn't be null."); // 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 _logger.LogError("The token request was rejected because the authorization code " +
// it was compromised. In this case, revoke the authorization and all the associated tokens. "or refresh token '{Identifier}' has already been redeemed.", identifier);
// 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 " + context.Reject(
"or refresh token '{Identifier}' has already been redeemed.", identifier); 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( return;
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; 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 " + // Extract the authorization identifier from the authentication ticket.
"or refresh token '{Identifier}' was no longer valid.", identifier); 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( context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant, error: OpenIdConnectConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ? description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." : "The authorization associated with the authorization code is no longer valid." :
"The specified refresh token 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 // Invoke the rest of the pipeline to allow

8
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs

@ -83,8 +83,8 @@ namespace OpenIddict.Server
[NotNull] OpenIdConnectRequest request, [NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format) [NotNull] ISecureDataFormat<AuthenticationTicket> format)
{ {
Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), Debug.Assert(!(options.DisableTokenStorage && options.UseReferenceTokens),
"Token revocation cannot be disabled when using reference tokens."); "Token storage cannot be disabled when using reference tokens.");
Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken ||
type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || type == OpenIdConnectConstants.TokenUsages.AuthorizationCode ||
@ -103,7 +103,7 @@ namespace OpenIddict.Server
ticket.Properties.ExpiresUtc = properties.ExpiresUtc; ticket.Properties.ExpiresUtc = properties.ExpiresUtc;
} }
if (options.DisableTokenRevocation) if (options.DisableTokenStorage)
{ {
return null; return null;
} }
@ -216,7 +216,7 @@ namespace OpenIddict.Server
[NotNull] OpenIdConnectRequest request, [NotNull] OpenIdConnectRequest request,
[NotNull] ISecureDataFormat<AuthenticationTicket> format) [NotNull] ISecureDataFormat<AuthenticationTicket> format)
{ {
Debug.Assert(!(options.DisableTokenRevocation && options.UseReferenceTokens), Debug.Assert(!(options.DisableTokenStorage && options.UseReferenceTokens),
"Token revocation cannot be disabled when using reference tokens."); "Token revocation cannot be disabled when using reference tokens.");
Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken || Debug.Assert(type == OpenIdConnectConstants.TokenUsages.AccessToken ||

40
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs

@ -138,24 +138,40 @@ namespace OpenIddict.Server
return; return;
} }
// If the received token is not a reference access token, // If an authorization was attached to the access token, ensure it is still valid.
// skip the additional reference token validation checks. if (!options.DisableAuthorizationStorage &&
if (!options.UseReferenceTokens) 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. return;
var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); }
Debug.Assert(token != null, "The token shouldn't be null."); }
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); await base.HandleIntrospectionRequest(context);

2
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs

@ -22,7 +22,7 @@ namespace OpenIddict.Server
{ {
var options = (OpenIddictServerOptions) context.Options; 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. // When token_type_hint is specified, reject the request if it doesn't correspond to a revocable token.
if (!string.IsNullOrEmpty(context.Request.TokenTypeHint)) if (!string.IsNullOrEmpty(context.Request.TokenTypeHint))

12
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs

@ -17,7 +17,7 @@ namespace OpenIddict.Server
public override async Task DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context) public override async Task DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context)
{ {
var options = (OpenIddictServerOptions) context.Options; var options = (OpenIddictServerOptions) context.Options;
if (options.DisableTokenRevocation) if (options.DisableTokenStorage)
{ {
return; return;
} }
@ -40,7 +40,7 @@ namespace OpenIddict.Server
public override async Task DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context) public override async Task DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context)
{ {
var options = (OpenIddictServerOptions) context.Options; var options = (OpenIddictServerOptions) context.Options;
if (options.DisableTokenRevocation) if (options.DisableTokenStorage)
{ {
return; return;
} }
@ -59,7 +59,7 @@ namespace OpenIddict.Server
public override async Task DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context) public override async Task DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context)
{ {
var options = (OpenIddictServerOptions) context.Options; var options = (OpenIddictServerOptions) context.Options;
if (options.DisableTokenRevocation) if (options.DisableTokenStorage)
{ {
return; return;
} }
@ -78,7 +78,7 @@ namespace OpenIddict.Server
public override async Task SerializeAccessToken([NotNull] SerializeAccessTokenContext context) public override async Task SerializeAccessToken([NotNull] SerializeAccessTokenContext context)
{ {
var options = (OpenIddictServerOptions) context.Options; var options = (OpenIddictServerOptions) context.Options;
if (options.DisableTokenRevocation) if (options.DisableTokenStorage)
{ {
return; return;
} }
@ -104,7 +104,7 @@ namespace OpenIddict.Server
public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context)
{ {
var options = (OpenIddictServerOptions) context.Options; var options = (OpenIddictServerOptions) context.Options;
if (options.DisableTokenRevocation) if (options.DisableTokenStorage)
{ {
return; return;
} }
@ -132,7 +132,7 @@ namespace OpenIddict.Server
public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context)
{ {
var options = (OpenIddictServerOptions) context.Options; var options = (OpenIddictServerOptions) context.Options;
if (options.DisableTokenRevocation) if (options.DisableTokenStorage)
{ {
return; return;
} }

84
src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs

@ -117,56 +117,51 @@ namespace OpenIddict.Server
context.IncludeRefreshToken &= options.UseRollingTokens; context.IncludeRefreshToken &= options.UseRollingTokens;
} }
// If token revocation was explicitly disabled, // If token revocation was explicitly disabled, none of the following security routines apply.
// none of the following security routines apply. if (!options.DisableTokenStorage)
if (options.DisableTokenRevocation)
{ {
await base.ProcessSigninResponse(context); var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{context.Ticket.GetTokenId()}");
Debug.Assert(token != null, "The token shouldn't be null.");
return;
} // 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.
var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{context.Ticket.GetTokenId()}"); // If the operation fails, return an error indicating the code/token is no longer valid.
Debug.Assert(token != null, "The token shouldn't be null."); // See https://tools.ietf.org/html/rfc6749#section-6 for more information.
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
// 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.Reject( if (!await TryRedeemTokenAsync(token))
error: OpenIdConnectConstants.Errors.InvalidGrant, {
description: context.Request.IsAuthorizationCodeGrantType() ? context.Reject(
"The specified authorization code is no longer valid." : error: OpenIdConnectConstants.Errors.InvalidGrant,
"The specified refresh token is no longer valid."); 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()) 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)
{ {
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 // When rolling tokens are disabled, try to extend the expiration date
// of the existing token instead of returning a new refresh token // of the existing token instead of returning a new refresh token
// with a new expiration date if sliding expiration was not disabled. // with a new expiration date if sliding expiration was not disabled.
// If the operation fails, silently ignore the error and keep processing // If the operation fails, silently ignore the error and keep processing
// the request: this may indicate that a concurrent refresh token request // the request: this may indicate that a concurrent refresh token request
// already updated the expiration date associated with the refresh token. // already updated the expiration date associated with the refresh token.
if (!options.UseRollingTokens && options.UseSlidingExpiration) if (!options.UseRollingTokens && options.UseSlidingExpiration)
{ {
await TryExtendTokenAsync(token, context.Ticket, options); await TryExtendTokenAsync(token, context.Ticket, options);
}
} }
} }
} }
@ -174,7 +169,8 @@ namespace OpenIddict.Server
// If no authorization was explicitly attached to the authentication ticket, // If no authorization was explicitly attached to the authentication ticket,
// create an ad hoc authorization if an authorization code or a refresh token // 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. // 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)) (context.IncludeAuthorizationCode || context.IncludeRefreshToken))
{ {
await CreateAuthorizationAsync(context.Ticket, options, context.Request); await CreateAuthorizationAsync(context.Ticket, options, context.Request);

20
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -341,6 +341,16 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerBuilder AllowRefreshTokenFlow() public OpenIddictServerBuilder AllowRefreshTokenFlow()
=> Configure(options => options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.RefreshToken)); => Configure(options => options.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.RefreshToken));
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder DisableAuthorizationStorage()
=> Configure(options => options.DisableAuthorizationStorage = true);
/// <summary> /// <summary>
/// Disables the configuration endpoint. /// Disables the configuration endpoint.
/// </summary> /// </summary>
@ -364,7 +374,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// <summary> /// <summary>
/// Disables sliding expiration. When using this option, refresh tokens /// 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. /// authorization flow must be started to retrieve a new refresh token.
/// </summary> /// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
@ -372,13 +382,13 @@ namespace Microsoft.Extensions.DependencyInjection
=> Configure(options => options.UseSlidingExpiration = false); => Configure(options => options.UseSlidingExpiration = false);
/// <summary> /// <summary>
/// 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. /// refresh tokens are never stored and cannot be revoked.
/// Using this option is generally not recommended. /// Using this option is generally NOT recommended.
/// </summary> /// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder DisableTokenRevocation() public OpenIddictServerBuilder DisableTokenStorage()
=> Configure(options => options.DisableTokenRevocation = true); => Configure(options => options.DisableTokenStorage = true);
/// <summary> /// <summary>
/// Enables the authorization endpoint. /// Enables the authorization endpoint.

9
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -37,11 +37,10 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddScoped<OpenIddictServerHandler>(); builder.Services.TryAddScoped<OpenIddictServerHandler>();
builder.Services.TryAddScoped(provider => builder.Services.TryAddScoped(provider =>
{ {
InvalidOperationException CreateException() InvalidOperationException CreateException() => new InvalidOperationException(new StringBuilder()
=> new InvalidOperationException(new StringBuilder() .AppendLine("The core services must be registered when enabling the server handler.")
.AppendLine("The core services must be registered when enabling the server handler.") .Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.")
.Append("To register the OpenIddict core services, use 'services.AddOpenIddict().AddCore()'.") .ToString());
.ToString());
return new OpenIddictServerProvider( return new OpenIddictServerProvider(
provider.GetRequiredService<ILogger<OpenIddictServerProvider>>(), provider.GetRequiredService<ILogger<OpenIddictServerProvider>>(),

11
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -61,11 +61,18 @@ namespace OpenIddict.Server
}; };
/// <summary> /// <summary>
/// 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.
/// </summary>
public bool DisableAuthorizationStorage { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether token storage should be disabled.
/// When disabled, authorization code and refresh tokens are not stored /// When disabled, authorization code and refresh tokens are not stored
/// and cannot be revoked. Using this option is generally not recommended. /// and cannot be revoked. Using this option is generally not recommended.
/// </summary> /// </summary>
public bool DisableTokenRevocation { get; set; } public bool DisableTokenStorage { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether request caching should be enabled. /// Gets or sets a boolean indicating whether request caching should be enabled.

44
test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs

@ -5,6 +5,7 @@
*/ */
using System; using System;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Client; using AspNet.Security.OpenIdConnect.Client;
using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Primitives;
@ -127,8 +128,7 @@ namespace OpenIddict.Server.Tests
return client.GetAsync("/"); return client.GetAsync("/");
}); });
Assert.Equal("The authorization endpoint must be enabled to use " + Assert.Equal("The authorization endpoint must be enabled to use the authorization code and implicit flows.", exception.Message);
"the authorization code and implicit flows.", exception.Message);
} }
[Theory] [Theory]
@ -159,7 +159,7 @@ namespace OpenIddict.Server.Tests
} }
[Fact] [Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenTokenRevocationIsDisabled() public async Task PostConfigure_ThrowsAnExceptionWhenTokenStorageIsDisabled()
{ {
// Arrange // Arrange
var server = CreateAuthorizationServer(builder => var server = CreateAuthorizationServer(builder =>
@ -167,7 +167,7 @@ namespace OpenIddict.Server.Tests
builder.EnableAuthorizationEndpoint("/connect/authorize") builder.EnableAuthorizationEndpoint("/connect/authorize")
.EnableRevocationEndpoint("/connect/revocation") .EnableRevocationEndpoint("/connect/revocation")
.AllowImplicitFlow() .AllowImplicitFlow()
.DisableTokenRevocation(); .DisableTokenStorage();
}); });
var client = new OpenIdConnectClient(server.CreateClient()); var client = new OpenIdConnectClient(server.CreateClient());
@ -178,18 +178,18 @@ namespace OpenIddict.Server.Tests
return client.GetAsync("/"); 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] [Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenRevocationDisabled() public async Task PostConfigure_ThrowsAnExceptionWhenUsingReferenceTokensWithTokenStorageDisabled()
{ {
// Arrange // Arrange
var server = CreateAuthorizationServer(builder => var server = CreateAuthorizationServer(builder =>
{ {
builder.EnableAuthorizationEndpoint("/connect/authorize") builder.EnableAuthorizationEndpoint("/connect/authorize")
.AllowImplicitFlow() .AllowImplicitFlow()
.DisableTokenRevocation() .DisableTokenStorage()
.UseReferenceTokens(); .UseReferenceTokens();
}); });
@ -201,18 +201,18 @@ namespace OpenIddict.Server.Tests
return client.GetAsync("/"); 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] [Fact]
public async Task PostConfigure_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenRevocationDisabled() public async Task PostConfigure_ThrowsAnExceptionWhenUsingSlidingExpirationWithoutRollingTokensAndWithTokenStorageDisabled()
{ {
// Arrange // Arrange
var server = CreateAuthorizationServer(builder => var server = CreateAuthorizationServer(builder =>
{ {
builder.EnableAuthorizationEndpoint("/connect/authorize") builder.EnableAuthorizationEndpoint("/connect/authorize")
.AllowImplicitFlow() .AllowImplicitFlow()
.DisableTokenRevocation(); .DisableTokenStorage();
}); });
var client = new OpenIdConnectClient(server.CreateClient()); var client = new OpenIdConnectClient(server.CreateClient());
@ -224,7 +224,7 @@ namespace OpenIddict.Server.Tests
}); });
Assert.Equal("Sliding expiration must be disabled when turning off " + 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] [Fact]
@ -270,11 +270,12 @@ namespace OpenIddict.Server.Tests
return client.GetAsync("/"); return client.GetAsync("/");
}); });
Assert.Equal( Assert.Equal(new StringBuilder()
"At least one signing key must be registered when using JWT as the access token format. " + .AppendLine("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()' " + .Append("Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' ")
"or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + .Append("or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call ")
"'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); .Append("'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.")
.ToString(), exception.Message);
} }
[Fact] [Fact]
@ -295,11 +296,12 @@ namespace OpenIddict.Server.Tests
return client.GetAsync("/"); return client.GetAsync("/");
}); });
Assert.Equal( Assert.Equal(new StringBuilder()
"At least one asymmetric signing key must be registered when enabling the implicit flow. " + .AppendLine("At least one asymmetric signing key must be registered when enabling the implicit flow.")
"Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + .Append("Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' ")
"or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + .Append("or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call ")
"'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); .Append("'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.")
.ToString(), exception.Message);
} }
private static TestServer CreateAuthorizationServer(Action<OpenIddictServerBuilder> configuration = null) private static TestServer CreateAuthorizationServer(Action<OpenIddictServerBuilder> configuration = null)

562
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs

@ -736,7 +736,7 @@ namespace OpenIddict.Server.Tests
} }
[Fact] [Fact]
public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenRevocationIsDisabled() public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenStorageIsDisabled()
{ {
// Arrange // Arrange
var ticket = new AuthenticationTicket( var ticket = new AuthenticationTicket(
@ -777,7 +777,7 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => options.AuthorizationCodeFormat = format.Object); builder.Configure(options => options.AuthorizationCodeFormat = format.Object);
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
builder.DisableTokenRevocation(); builder.DisableTokenStorage();
builder.DisableSlidingExpiration(); builder.DisableSlidingExpiration();
}); });
@ -797,7 +797,7 @@ namespace OpenIddict.Server.Tests
} }
[Fact] [Fact]
public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenRevocationIsDisabled() public async Task HandleTokenRequest_RefreshTokenRevocationIsIgnoredWhenTokenStorageIsDisabled()
{ {
// Arrange // Arrange
var ticket = new AuthenticationTicket( var ticket = new AuthenticationTicket(
@ -837,7 +837,7 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => options.RefreshTokenFormat = format.Object); builder.Configure(options => options.RefreshTokenFormat = format.Object);
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
builder.DisableTokenRevocation(); builder.DisableTokenStorage();
builder.DisableSlidingExpiration(); builder.DisableSlidingExpiration();
}); });
@ -1682,6 +1682,546 @@ namespace OpenIddict.Server.Tests
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA"))
.Returns(ticket);
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictAuthorization());
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Public));
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
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<CancellationToken>()))
.ReturnsAsync(new OpenIddictAuthorization());
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Public));
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA"))
.Returns(ticket);
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Public));
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
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<CancellationToken>()))
.ReturnsAsync(authorization);
instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Public));
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("8xLOxBtZp8"))
.Returns(ticket);
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Public));
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56"));
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
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<CancellationToken>()))
.ReturnsAsync(authorization);
instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Public));
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("60FFF7EA-F98E-437B-937E-5073CC313103951EFBA23A56"));
instance.Setup(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()), Times.Once());
}
[Theory] [Theory]
[InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)]
[InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)]
@ -1730,6 +2270,9 @@ namespace OpenIddict.Server.Tests
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true); .ReturnsAsync(true);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0"));
}); });
var server = CreateAuthorizationServer(builder => var server = CreateAuthorizationServer(builder =>
@ -1772,6 +2315,17 @@ namespace OpenIddict.Server.Tests
.ReturnsAsync(true); .ReturnsAsync(true);
})); }));
builder.Services.AddSingleton(CreateAuthorizationManager(instance =>
{
var authorization = new OpenIddictAuthorization();
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(authorization);
instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant");
builder.Services.AddSingleton(manager); builder.Services.AddSingleton(manager);

289
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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"))
.Returns(ticket);
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(new OpenIddictAuthorization());
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Confidential));
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("2YotnFZFEjr1zCsicMWpAA"));
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
format.Setup(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"))
.Returns(ticket);
var manager = CreateAuthorizationManager(instance =>
{
instance.Setup(mock => mock.FindByIdAsync("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0", It.IsAny<CancellationToken>()))
.ReturnsAsync(value: null);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Confidential));
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("2YotnFZFEjr1zCsicMWpAA"));
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
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<CancellationToken>()))
.ReturnsAsync(authorization);
instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Introspection, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Confidential));
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
var token = new OpenIddictToken();
instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("2YotnFZFEjr1zCsicMWpAA"));
instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
instance.Setup(mock => mock.GetAuthorizationIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact] [Fact]
public async Task HandleIntrospectionRequest_RequestIsRejectedWhenReferenceTokenIsInvalid() public async Task HandleIntrospectionRequest_RequestIsRejectedWhenReferenceTokenIsInvalid()
{ {

4
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs

@ -1791,7 +1791,7 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
builder.DisableTokenRevocation(); builder.DisableTokenStorage();
builder.DisableSlidingExpiration(); builder.DisableSlidingExpiration();
}); });
@ -2195,7 +2195,7 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.Configure(options => options.RevocationEndpointPath = PathString.Empty);
builder.DisableTokenRevocation(); builder.DisableTokenStorage();
builder.DisableSlidingExpiration(); builder.DisableSlidingExpiration();
}); });

101
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs

@ -765,7 +765,6 @@ namespace OpenIddict.Server.Tests
ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103"); ticket.SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103");
ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken); ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.RefreshToken);
ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess); ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.OfflineAccess);
ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "18D15F73-BE2B-6867-DC01-B3C1E8AFDED0");
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); var format = new Mock<ISecureDataFormat<AuthenticationTicket>>();
@ -809,6 +808,17 @@ namespace OpenIddict.Server.Tests
var server = CreateAuthorizationServer(builder => 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<CancellationToken>()))
.ReturnsAsync(authorization);
instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager); builder.Services.AddSingleton(manager);
builder.UseRollingTokens(); builder.UseRollingTokens();
@ -887,6 +897,17 @@ namespace OpenIddict.Server.Tests
var server = CreateAuthorizationServer(builder => 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<CancellationToken>()))
.ReturnsAsync(authorization);
instance.Setup(mock => mock.IsValidAsync(authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
builder.Services.AddSingleton(manager); builder.Services.AddSingleton(manager);
builder.Configure(options => options.RefreshTokenFormat = format.Object); builder.Configure(options => options.RefreshTokenFormat = format.Object);
@ -902,6 +923,7 @@ namespace OpenIddict.Server.Tests
}); });
// Assert // Assert
Assert.NotNull(response.AccessToken);
Assert.Null(response.RefreshToken); Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once());
@ -1188,6 +1210,75 @@ namespace OpenIddict.Server.Tests
It.IsAny<CancellationToken>()), Times.Once()); It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(new OpenIddictAuthorization());
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.Endpoints.Authorization, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.HasPermissionAsync(application,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Public));
instance.Setup(mock => mock.GetIdAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
}));
builder.Services.AddSingleton(CreateTokenManager(instance =>
{
instance.Setup(mock => mock.CreateAsync(It.IsAny<OpenIddictTokenDescriptor>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("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<OpenIddictAuthorizationDescriptor>(), It.IsAny<CancellationToken>()), Times.Never());
}
[Fact] [Fact]
public async Task ProcessSigninResponse_CustomPublicParametersAreAddedToAuthorizationResponse() public async Task ProcessSigninResponse_CustomPublicParametersAreAddedToAuthorizationResponse()
{ {
@ -1351,10 +1442,10 @@ namespace OpenIddict.Server.Tests
.SetDefaultScopeEntity<OpenIddictScope>() .SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>(); .SetDefaultTokenEntity<OpenIddictToken>();
options.Services.AddSingleton(CreateApplicationManager()); options.Services.AddSingleton(CreateApplicationManager())
options.Services.AddSingleton(CreateAuthorizationManager()); .AddSingleton(CreateAuthorizationManager())
options.Services.AddSingleton(CreateScopeManager()); .AddSingleton(CreateScopeManager())
options.Services.AddSingleton(CreateTokenManager()); .AddSingleton(CreateTokenManager());
}) })
.AddServer(options => .AddServer(options =>

22
test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

@ -296,6 +296,22 @@ namespace OpenIddict.Server.Tests
Assert.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken, options.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] [Fact]
public void DisableConfigurationEndpoint_ConfigurationEndpointIsDisabled() public void DisableConfigurationEndpoint_ConfigurationEndpointIsDisabled()
{ {
@ -345,19 +361,19 @@ namespace OpenIddict.Server.Tests
} }
[Fact] [Fact]
public void DisableTokenRevocation_TokenRevocationIsDisabled() public void DisableTokenStorage_TokenStorageIsDisabled()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act // Act
builder.DisableTokenRevocation(); builder.DisableTokenStorage();
var options = GetOptions(services); var options = GetOptions(services);
// Assert // Assert
Assert.True(options.DisableTokenRevocation); Assert.True(options.DisableTokenStorage);
} }
[Fact] [Fact]

Loading…
Cancel
Save