Browse Source

Update the token validation logic to validate JWT tokens only once, independently of their actual type

pull/856/head
Kévin Chalet 7 years ago
parent
commit
d5e449d065
  1. 9
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  2. 42
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  3. 25
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  4. 11
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs
  5. 48
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  6. 86
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  7. 124
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  8. 5
      src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs
  9. 13
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  10. 2
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  11. 72
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs

9
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -304,6 +304,15 @@ namespace OpenIddict.Abstractions
/// <returns><c>true</c> if the token has the specified status, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the token has the specified status, <c>false</c> otherwise.</returns>
ValueTask<bool> HasStatusAsync([NotNull] object token, [NotNull] string status, CancellationToken cancellationToken = default); ValueTask<bool> HasStatusAsync([NotNull] object token, [NotNull] string status, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given token has the specified type.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="type">The expected type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token has the specified type, <c>false</c> otherwise.</returns>
ValueTask<bool> HasTypeAsync([NotNull] object token, [NotNull] string type, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>

42
src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs

@ -1299,8 +1299,7 @@ namespace OpenIddict.Abstractions
=> principal.GetClaim(Claims.Private.TokenUsage); => principal.GetClaim(Claims.Private.TokenUsage);
/// <summary> /// <summary>
/// Gets a boolean value indicating whether the /// Gets a boolean value indicating whether the claims principal corresponds to an access token.
/// claims principal corresponds to an access token.
/// </summary> /// </summary>
/// <param name="principal">The claims principal.</param> /// <param name="principal">The claims principal.</param>
/// <returns><c>true</c> if the principal corresponds to an access token.</returns> /// <returns><c>true</c> if the principal corresponds to an access token.</returns>
@ -1315,8 +1314,7 @@ namespace OpenIddict.Abstractions
} }
/// <summary> /// <summary>
/// Gets a boolean value indicating whether the /// Gets a boolean value indicating whether the claims principal corresponds to an access token.
/// claims principal corresponds to an access token.
/// </summary> /// </summary>
/// <param name="principal">The claims principal.</param> /// <param name="principal">The claims principal.</param>
/// <returns><c>true</c> if the principal corresponds to an authorization code.</returns> /// <returns><c>true</c> if the principal corresponds to an authorization code.</returns>
@ -1331,8 +1329,22 @@ namespace OpenIddict.Abstractions
} }
/// <summary> /// <summary>
/// Gets a boolean value indicating whether the /// Gets a boolean value indicating whether the claims principal corresponds to a device code.
/// claims principal corresponds to an identity token. /// </summary>
/// <param name="principal">The claims principal.</param>
/// <returns><c>true</c> if the principal corresponds to a device code.</returns>
public static bool IsDeviceCode([NotNull] this ClaimsPrincipal principal)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
return string.Equals(principal.GetTokenUsage(), TokenUsages.DeviceCode, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Gets a boolean value indicating whether the claims principal corresponds to an identity token.
/// </summary> /// </summary>
/// <param name="principal">The claims principal.</param> /// <param name="principal">The claims principal.</param>
/// <returns><c>true</c> if the principal corresponds to an identity token.</returns> /// <returns><c>true</c> if the principal corresponds to an identity token.</returns>
@ -1347,8 +1359,7 @@ namespace OpenIddict.Abstractions
} }
/// <summary> /// <summary>
/// Gets a boolean value indicating whether the /// Gets a boolean value indicating whether the claims principal corresponds to a refresh token.
/// claims principal corresponds to a refresh token.
/// </summary> /// </summary>
/// <param name="principal">The claims principal.</param> /// <param name="principal">The claims principal.</param>
/// <returns><c>true</c> if the principal corresponds to a refresh token.</returns> /// <returns><c>true</c> if the principal corresponds to a refresh token.</returns>
@ -1362,6 +1373,21 @@ namespace OpenIddict.Abstractions
return string.Equals(principal.GetTokenUsage(), TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase); return string.Equals(principal.GetTokenUsage(), TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// Gets a boolean value indicating whether the claims principal corresponds to a user code.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <returns><c>true</c> if the principal corresponds to a user code.</returns>
public static bool IsUserCode([NotNull] this ClaimsPrincipal principal)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
return string.Equals(principal.GetTokenUsage(), TokenUsages.UserCode, StringComparison.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// Determines whether the claims principal contains at least one audience. /// Determines whether the claims principal contains at least one audience.
/// </summary> /// </summary>

25
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -757,6 +757,28 @@ namespace OpenIddict.Core
return string.Equals(await Store.GetStatusAsync(token, cancellationToken), status, StringComparison.OrdinalIgnoreCase); return string.Equals(await Store.GetStatusAsync(token, cancellationToken), status, StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// Determines whether a given token has the specified type.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="type">The expected type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token has the specified type, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> HasTypeAsync([NotNull] TToken token, [NotNull] string type, CancellationToken cancellationToken = default)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (string.IsNullOrEmpty(type))
{
throw new ArgumentException("The type cannot be null or empty.", nameof(type));
}
return string.Equals(await Store.GetTypeAsync(token, cancellationToken), type, StringComparison.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -1366,6 +1388,9 @@ namespace OpenIddict.Core
ValueTask<bool> IOpenIddictTokenManager.HasStatusAsync(object token, string status, CancellationToken cancellationToken) ValueTask<bool> IOpenIddictTokenManager.HasStatusAsync(object token, string status, CancellationToken cancellationToken)
=> HasStatusAsync((TToken) token, status, cancellationToken); => HasStatusAsync((TToken) token, status, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.HasTypeAsync(object token, string type, CancellationToken cancellationToken)
=> HasTypeAsync((TToken) token, type, cancellationToken);
IAsyncEnumerable<object> IOpenIddictTokenManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) IAsyncEnumerable<object> IOpenIddictTokenManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken)
=> ListAsync(count, offset, cancellationToken).OfType<object>(); => ListAsync(count, offset, cancellationToken).OfType<object>();

11
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs

@ -17,7 +17,6 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes; using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters;
using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerEvents;
@ -87,9 +86,11 @@ namespace OpenIddict.Server.DataProtection
// If the token cannot be validated, don't return an error to allow another handle to validate it. // If the token cannot be validated, don't return an error to allow another handle to validate it.
var principal = !string.IsNullOrEmpty(context.TokenType) ? var principal = !string.IsNullOrEmpty(context.TokenType) ?
ValidateToken(context.Token, context.TokenType) : ValidateToken(context.Token, context.TokenType) :
ValidateToken(context.Token, TokenUsages.AccessToken) ?? ValidateToken(context.Token, TokenUsages.AccessToken) ??
ValidateToken(context.Token, TokenUsages.RefreshToken) ?? ValidateToken(context.Token, TokenUsages.RefreshToken) ??
ValidateToken(context.Token, TokenUsages.AuthorizationCode); ValidateToken(context.Token, TokenUsages.AuthorizationCode) ??
ValidateToken(context.Token, TokenUsages.DeviceCode) ??
ValidateToken(context.Token, TokenUsages.UserCode);
if (principal == null) if (principal == null)
{ {
return default; return default;
@ -120,7 +121,7 @@ namespace OpenIddict.Server.DataProtection
=> new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server }, => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server },
TokenUsages.UserCode when !context.Options.DisableTokenStorage TokenUsages.UserCode when !context.Options.DisableTokenStorage
=> new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server, }, => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server },
TokenUsages.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, TokenUsages.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server },
TokenUsages.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server }, TokenUsages.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server },

48
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -48,6 +48,7 @@ namespace OpenIddict.Server
ValidateClientSecret.Descriptor, ValidateClientSecret.Descriptor,
ValidateEndpointPermissions.Descriptor, ValidateEndpointPermissions.Descriptor,
ValidateToken.Descriptor, ValidateToken.Descriptor,
ValidateTokenType.Descriptor,
ValidateAuthorizedParty.Descriptor, ValidateAuthorizedParty.Descriptor,
/* /*
@ -808,6 +809,51 @@ namespace OpenIddict.Server
} }
} }
/// <summary>
/// Contains the logic responsible of rejecting introspection requests that specify an unsupported token.
/// </summary>
public class ValidateTokenType : IOpenIddictServerHandler<ValidateIntrospectionRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateIntrospectionRequestContext>()
.UseSingletonHandler<ValidateTokenType>()
.SetOrder(ValidateToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.Principal.IsAccessToken() && !context.Principal.IsAuthorizationCode() &&
!context.Principal.IsIdentityToken() && !context.Principal.IsRefreshToken())
{
context.Logger.LogError("The introspection request was rejected because " +
"the received token was of an unsupported type.");
context.Reject(
error: Errors.UnsupportedTokenType,
description: "The specified token cannot be introspected.");
return default;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible of rejecting introspection requests that specify a token /// Contains the logic responsible of rejecting introspection requests that specify a token
/// that cannot be introspected by the client application sending the introspection requests. /// that cannot be introspected by the client application sending the introspection requests.
@ -824,7 +870,7 @@ namespace OpenIddict.Server
// In this case, the returned claims are limited by AttachApplicationClaims to limit exposure. // In this case, the returned claims are limited by AttachApplicationClaims to limit exposure.
.AddFilter<RequireClientIdParameter>() .AddFilter<RequireClientIdParameter>()
.UseSingletonHandler<ValidateAuthorizedParty>() .UseSingletonHandler<ValidateAuthorizedParty>()
.SetOrder(ValidateToken.Descriptor.Order + 1_000) .SetOrder(ValidateTokenType.Descriptor.Order + 1_000)
.Build(); .Build();
/// <summary> /// <summary>

86
src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs

@ -41,6 +41,7 @@ namespace OpenIddict.Server
ValidateClientSecret.Descriptor, ValidateClientSecret.Descriptor,
ValidateEndpointPermissions.Descriptor, ValidateEndpointPermissions.Descriptor,
ValidateToken.Descriptor, ValidateToken.Descriptor,
ValidateTokenType.Descriptor,
ValidateAuthorizedParty.Descriptor, ValidateAuthorizedParty.Descriptor,
/* /*
@ -754,6 +755,64 @@ namespace OpenIddict.Server
} }
} }
/// <summary>
/// Contains the logic responsible of rejecting revocation requests that specify an unsupported token.
/// </summary>
public class ValidateTokenType : IOpenIddictServerHandler<ValidateRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateRevocationRequestContext>()
.UseSingletonHandler<ValidateTokenType>()
.SetOrder(ValidateToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ValidateRevocationRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.Principal.IsAccessToken() &&
!context.Principal.IsAuthorizationCode() &&
!context.Principal.IsRefreshToken())
{
context.Logger.LogError("The revocation request was rejected because " +
"the received token was of an unsupported type.");
context.Reject(
error: Errors.UnsupportedTokenType,
description: "This token cannot be revoked.");
return default;
}
// If the received token is an access token, return an error if reference tokens are not enabled.
if (context.Principal.IsAccessToken() && !context.Options.UseReferenceAccessTokens)
{
context.Logger.LogError("The revocation request was rejected because the access token was not revocable.");
context.Reject(
error: Errors.UnsupportedTokenType,
description: "The specified token cannot be revoked.");
return default;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible of rejecting revocation requests that specify a token /// Contains the logic responsible of rejecting revocation requests that specify a token
/// that cannot be revoked by the client application sending the revocation requests. /// that cannot be revoked by the client application sending the revocation requests.
@ -770,7 +829,7 @@ namespace OpenIddict.Server
// In this case, the risk is quite limited as claims are never returned by this endpoint. // In this case, the risk is quite limited as claims are never returned by this endpoint.
.AddFilter<RequireClientIdParameter>() .AddFilter<RequireClientIdParameter>()
.UseSingletonHandler<ValidateAuthorizedParty>() .UseSingletonHandler<ValidateAuthorizedParty>()
.SetOrder(ValidateToken.Descriptor.Order + 1_000) .SetOrder(ValidateTokenType.Descriptor.Order + 1_000)
.Build(); .Build();
/// <summary> /// <summary>
@ -949,31 +1008,6 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// If the received token is not an authorization code or a refresh token,
// return an error to indicate that the token cannot be revoked.
if (context.Principal.IsIdentityToken())
{
context.Logger.LogError("The revocation request was rejected because identity tokens are not revocable.");
context.Reject(
error: Errors.UnsupportedTokenType,
description: "The specified token cannot be revoked.");
return;
}
// If the received token is an access token, return an error if reference tokens are not enabled.
if (context.Principal.IsAccessToken() && !context.Options.UseReferenceAccessTokens)
{
context.Logger.LogError("The revocation request was rejected because the access token was not revocable.");
context.Reject(
error: Errors.UnsupportedTokenType,
description: "The specified token cannot be revoked.");
return;
}
// Extract the token identifier from the authentication principal. // Extract the token identifier from the authentication principal.
var identifier = context.Principal.GetInternalTokenId(); var identifier = context.Principal.GetInternalTokenId();
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))

124
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -188,6 +188,8 @@ namespace OpenIddict.Server
OpenIddictServerEndpointType.Authorization => (context.Request.IdTokenHint, TokenUsages.IdToken), OpenIddictServerEndpointType.Authorization => (context.Request.IdTokenHint, TokenUsages.IdToken),
OpenIddictServerEndpointType.Logout => (context.Request.IdTokenHint, TokenUsages.IdToken), OpenIddictServerEndpointType.Logout => (context.Request.IdTokenHint, TokenUsages.IdToken),
// Generic tokens received by the introspection and revocation can be of any type.
// Additional token type filtering is made by the endpoint themselves, if needed.
OpenIddictServerEndpointType.Introspection => (context.Request.Token, null), OpenIddictServerEndpointType.Introspection => (context.Request.Token, null),
OpenIddictServerEndpointType.Revocation => (context.Request.Token, null), OpenIddictServerEndpointType.Revocation => (context.Request.Token, null),
@ -334,6 +336,7 @@ namespace OpenIddict.Server
return; return;
} }
// If the type associated with the token entry doesn't match the expected type, return an error.
if (!string.IsNullOrEmpty(context.TokenType) && if (!string.IsNullOrEmpty(context.TokenType) &&
!string.Equals(context.TokenType, await _tokenManager.GetTypeAsync(token))) !string.Equals(context.TokenType, await _tokenManager.GetTypeAsync(token)))
{ {
@ -416,41 +419,18 @@ namespace OpenIddict.Server
return; return;
} }
// If the token cannot be validated, don't return an error to allow another handle to validate it. var parameters = context.Options.TokenValidationParameters.Clone();
var result = !string.IsNullOrEmpty(context.TokenType) ? parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
await ValidateTokenAsync(context.Token, context.TokenType) : parameters.IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key);
await ValidateAnyTokenAsync(context.Token); parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key);
if (result.ClaimsIdentity == null)
{
return;
}
// Attach the principal extracted from the token to the parent event context. // If a specific token type is expected, override the default valid types to reject
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); // security tokens whose "typ" header doesn't match the expected token type.
if (!string.IsNullOrEmpty(context.TokenType))
// Store the token type as a special private claim.
context.Principal.SetClaim(Claims.Private.TokenUsage, ((JsonWebToken) result.SecurityToken).Typ switch
{ {
JsonWebTokenTypes.AccessToken => TokenUsages.AccessToken,
JsonWebTokenTypes.IdentityToken => TokenUsages.IdToken,
JsonWebTokenTypes.Private.AuthorizationCode => TokenUsages.AuthorizationCode,
JsonWebTokenTypes.Private.DeviceCode => TokenUsages.DeviceCode,
JsonWebTokenTypes.Private.RefreshToken => TokenUsages.RefreshToken,
JsonWebTokenTypes.Private.UserCode => TokenUsages.UserCode,
_ => throw new InvalidOperationException("The token type is not supported.")
});
context.Logger.LogTrace("The token '{Token}' was successfully validated and the following claims " +
"could be extracted: {Claims}.", context.Token, context.Principal.Claims);
async ValueTask<TokenValidationResult> ValidateTokenAsync(string token, string type)
{
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidTypes = new[] parameters.ValidTypes = new[]
{ {
type switch context.TokenType switch
{ {
TokenUsages.AccessToken => JsonWebTokenTypes.AccessToken, TokenUsages.AccessToken => JsonWebTokenTypes.AccessToken,
TokenUsages.IdToken => JsonWebTokenTypes.IdentityToken, TokenUsages.IdToken => JsonWebTokenTypes.IdentityToken,
@ -463,74 +443,36 @@ namespace OpenIddict.Server
_ => throw new InvalidOperationException("The token type is not supported.") _ => throw new InvalidOperationException("The token type is not supported.")
} }
}; };
parameters.ValidIssuer = context.Issuer?.AbsoluteUri; }
parameters.IssuerSigningKeys = type switch
{
TokenUsages.AccessToken => context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenUsages.AuthorizationCode => context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenUsages.DeviceCode => context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenUsages.RefreshToken => context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenUsages.UserCode => context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenUsages.IdToken => context.Options.SigningCredentials
.Select(credentials => credentials.Key)
.OfType<AsymmetricSecurityKey>(),
_ => Array.Empty<SecurityKey>()
};
parameters.TokenDecryptionKeys = type switch
{
TokenUsages.AuthorizationCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key),
TokenUsages.DeviceCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key),
TokenUsages.RefreshToken => context.Options.EncryptionCredentials.Select(credentials => credentials.Key),
TokenUsages.UserCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key),
TokenUsages.AccessToken => context.Options.EncryptionCredentials
.Select(credentials => credentials.Key)
.Where(key => key is SymmetricSecurityKey),
_ => Array.Empty<SecurityKey>()
};
var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(token, parameters); // If the token cannot be validated, don't return an error to allow another handle to validate it.
if (!result.IsValid) var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(context.Token, parameters);
{ if (result.ClaimsIdentity == null || !result.IsValid)
context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", token); {
} context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", context.Token);
return result; return;
} }
async ValueTask<TokenValidationResult> ValidateAnyTokenAsync(string token) // Attach the principal extracted from the token to the parent event context.
{ context.Principal = new ClaimsPrincipal(result.ClaimsIdentity);
var result = await ValidateTokenAsync(token, TokenUsages.AccessToken);
if (result.IsValid)
{
return result;
}
result = await ValidateTokenAsync(token, TokenUsages.RefreshToken); // Store the token type as a special private claim.
if (result.IsValid) context.Principal.SetClaim(Claims.Private.TokenUsage, ((JsonWebToken) result.SecurityToken).Typ switch
{ {
return result; JsonWebTokenTypes.AccessToken => TokenUsages.AccessToken,
} JsonWebTokenTypes.IdentityToken => TokenUsages.IdToken,
result = await ValidateTokenAsync(token, TokenUsages.AuthorizationCode); JsonWebTokenTypes.Private.AuthorizationCode => TokenUsages.AuthorizationCode,
if (result.IsValid) JsonWebTokenTypes.Private.DeviceCode => TokenUsages.DeviceCode,
{ JsonWebTokenTypes.Private.RefreshToken => TokenUsages.RefreshToken,
return result; JsonWebTokenTypes.Private.UserCode => TokenUsages.UserCode,
}
result = await ValidateTokenAsync(token, TokenUsages.IdToken); _ => throw new InvalidOperationException("The token type is not supported.")
if (result.IsValid) });
{
return result;
}
return new TokenValidationResult { IsValid = false }; context.Logger.LogTrace("The token '{Token}' was successfully validated and the following claims " +
} "could be extracted: {Claims}.", context.Token, context.Principal.Claims);
} }
} }

5
src/OpenIddict.Server/OpenIddictServerJsonWebTokenHandler.cs

@ -73,11 +73,6 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(parameters)); throw new ArgumentNullException(nameof(parameters));
} }
if (parameters.ValidTypes == null || !parameters.ValidTypes.Any())
{
throw new InvalidOperationException("The valid token types collection cannot be empty.");
}
if (!CanReadToken(token)) if (!CanReadToken(token))
{ {
return new ValueTask<TokenValidationResult>(new TokenValidationResult return new ValueTask<TokenValidationResult>(new TokenValidationResult

13
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenIddict.Server namespace OpenIddict.Server
{ {
@ -111,7 +112,17 @@ namespace OpenIddict.Server
RoleClaimType = OpenIddictConstants.Claims.Role, RoleClaimType = OpenIddictConstants.Claims.Role,
// Note: audience and lifetime are manually validated by OpenIddict itself. // Note: audience and lifetime are manually validated by OpenIddict itself.
ValidateAudience = false, ValidateAudience = false,
ValidateLifetime = false ValidateLifetime = false,
// Note: valid types can be overriden by OpenIddict depending on the received request.
ValidTypes = new[]
{
JsonWebTokenTypes.AccessToken,
JsonWebTokenTypes.IdentityToken,
JsonWebTokenTypes.Private.AuthorizationCode,
JsonWebTokenTypes.Private.DeviceCode,
JsonWebTokenTypes.Private.RefreshToken,
JsonWebTokenTypes.Private.UserCode
}
}; };
/// <summary> /// <summary>

2
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -212,7 +212,7 @@ namespace OpenIddict.Validation
// If the token cannot be validated, don't return an error to allow another handle to validate it. // If the token cannot be validated, don't return an error to allow another handle to validate it.
var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(context.Token, parameters); var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(context.Token, parameters);
if (result.ClaimsIdentity == null) if (result.ClaimsIdentity == null || !result.IsValid)
{ {
context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", context.Token); context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", context.Token);

72
test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs

@ -2106,8 +2106,10 @@ namespace OpenIddict.Abstractions.Tests.Primitives
[InlineData("unknown", false)] [InlineData("unknown", false)]
[InlineData(OpenIddictConstants.TokenUsages.AccessToken, true)] [InlineData(OpenIddictConstants.TokenUsages.AccessToken, true)]
[InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)] [InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)]
[InlineData(OpenIddictConstants.TokenUsages.DeviceCode, false)]
[InlineData(OpenIddictConstants.TokenUsages.IdToken, false)] [InlineData(OpenIddictConstants.TokenUsages.IdToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)] [InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.UserCode, false)]
public void IsAccessToken_ReturnsExpectedResult(string usage, bool result) public void IsAccessToken_ReturnsExpectedResult(string usage, bool result)
{ {
// Arrange // Arrange
@ -2137,8 +2139,10 @@ namespace OpenIddict.Abstractions.Tests.Primitives
[InlineData("unknown", false)] [InlineData("unknown", false)]
[InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)] [InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, true)] [InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, true)]
[InlineData(OpenIddictConstants.TokenUsages.DeviceCode, false)]
[InlineData(OpenIddictConstants.TokenUsages.IdToken, false)] [InlineData(OpenIddictConstants.TokenUsages.IdToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)] [InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.UserCode, false)]
public void IsAuthorizationCode_ReturnsExpectedResult(string usage, bool result) public void IsAuthorizationCode_ReturnsExpectedResult(string usage, bool result)
{ {
// Arrange // Arrange
@ -2151,6 +2155,39 @@ namespace OpenIddict.Abstractions.Tests.Primitives
Assert.Equal(result, principal.IsAuthorizationCode()); Assert.Equal(result, principal.IsAuthorizationCode());
} }
[Fact]
public void IsDeviceCode_ThrowsAnExceptionForNullPrincipal()
{
// Arrange
var principal = (ClaimsPrincipal) null;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => principal.IsDeviceCode());
Assert.Equal("principal", exception.ParamName);
}
[Theory]
[InlineData(null, false)]
[InlineData("unknown", false)]
[InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)]
[InlineData(OpenIddictConstants.TokenUsages.DeviceCode, true)]
[InlineData(OpenIddictConstants.TokenUsages.IdToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.UserCode, false)]
public void IsDeviceCode_ReturnsExpectedResult(string usage, bool result)
{
// Arrange
var identity = new ClaimsIdentity();
var principal = new ClaimsPrincipal(identity);
principal.SetClaim(OpenIddictConstants.Claims.Private.TokenUsage, usage);
// Act and assert
Assert.Equal(result, principal.IsDeviceCode());
}
[Fact] [Fact]
public void IsIdentityToken_ThrowsAnExceptionForNullPrincipal() public void IsIdentityToken_ThrowsAnExceptionForNullPrincipal()
{ {
@ -2168,8 +2205,10 @@ namespace OpenIddict.Abstractions.Tests.Primitives
[InlineData("unknown", false)] [InlineData("unknown", false)]
[InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)] [InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)] [InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)]
[InlineData(OpenIddictConstants.TokenUsages.DeviceCode, false)]
[InlineData(OpenIddictConstants.TokenUsages.IdToken, true)] [InlineData(OpenIddictConstants.TokenUsages.IdToken, true)]
[InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)] [InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.UserCode, false)]
public void IsIdentityToken_ReturnsExpectedResult(string usage, bool result) public void IsIdentityToken_ReturnsExpectedResult(string usage, bool result)
{ {
// Arrange // Arrange
@ -2213,6 +2252,39 @@ namespace OpenIddict.Abstractions.Tests.Primitives
Assert.Equal(result, principal.IsRefreshToken()); Assert.Equal(result, principal.IsRefreshToken());
} }
[Fact]
public void IsUserCode_ThrowsAnExceptionForNullPrincipal()
{
// Arrange
var principal = (ClaimsPrincipal) null;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => principal.IsUserCode());
Assert.Equal("principal", exception.ParamName);
}
[Theory]
[InlineData(null, false)]
[InlineData("unknown", false)]
[InlineData(OpenIddictConstants.TokenUsages.AccessToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.AuthorizationCode, false)]
[InlineData(OpenIddictConstants.TokenUsages.DeviceCode, false)]
[InlineData(OpenIddictConstants.TokenUsages.IdToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.RefreshToken, false)]
[InlineData(OpenIddictConstants.TokenUsages.UserCode, true)]
public void IsUserCode_ReturnsExpectedResult(string usage, bool result)
{
// Arrange
var identity = new ClaimsIdentity();
var principal = new ClaimsPrincipal(identity);
principal.SetClaim(OpenIddictConstants.Claims.Private.TokenUsage, usage);
// Act and assert
Assert.Equal(result, principal.IsUserCode());
}
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("")] [InlineData("")]

Loading…
Cancel
Save