diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 9e81730e..e32e2b32 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -214,6 +214,11 @@ namespace OpenIddict.Abstractions public const string AccessToken = "at+jwt"; public const string IdentityToken = "JWT"; + public static class Prefixes + { + public const string Application = "application/"; + } + public static class Private { public const string AuthorizationCode = "oi_auc+jwt"; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 45c7b52a..c91a0ae3 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -446,27 +446,40 @@ namespace OpenIddict.Server var parameters = context.Options.TokenValidationParameters.Clone(); parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; - - // If a specific token type is expected, override the default valid types to reject - // security tokens whose actual token type doesn't match the expected token type. - if (!string.IsNullOrEmpty(context.TokenType)) + parameters.ValidTypes = context.TokenType switch { - parameters.ValidTypes = new[] + // If no specific token type is expected, accept all token types at this stage. + // Additional filtering can be made based on the resolved/actual token type. + var type when string.IsNullOrEmpty(type) => Array.Empty(), + + // For access tokens, both "at+jwt" and "application/at+jwt" are valid. + TokenTypeHints.AccessToken => new[] { - context.TokenType switch - { - TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken, - TokenTypeHints.IdToken => JsonWebTokenTypes.IdentityToken, + JsonWebTokenTypes.AccessToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken + }, - TokenTypeHints.AuthorizationCode => JsonWebTokenTypes.Private.AuthorizationCode, - TokenTypeHints.DeviceCode => JsonWebTokenTypes.Private.DeviceCode, - TokenTypeHints.RefreshToken => JsonWebTokenTypes.Private.RefreshToken, - TokenTypeHints.UserCode => JsonWebTokenTypes.Private.UserCode, + // For identity tokens, both "JWT" and "application/jwt" are valid. + TokenTypeHints.IdToken => new[] + { + JsonWebTokenTypes.IdentityToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken + }, - _ => throw new InvalidOperationException("The token type is not supported.") - } - }; - } + // For authorization codes, only the short "oi_auc+jwt" form is valid. + TokenTypeHints.AuthorizationCode => new[] { JsonWebTokenTypes.Private.AuthorizationCode }, + + // For device codes, only the short "oi_dvc+jwt" form is valid. + TokenTypeHints.DeviceCode => new[] { JsonWebTokenTypes.Private.DeviceCode }, + + // For refresh tokens, only the short "oi_reft+jwt" form is valid. + TokenTypeHints.RefreshToken => new[] { JsonWebTokenTypes.Private.RefreshToken }, + + // For user codes, only the short "oi_usrc+jwt" form is valid. + TokenTypeHints.UserCode => new[] { JsonWebTokenTypes.Private.UserCode }, + + _ => throw new InvalidOperationException("The token type is not supported.") + }; // If the token cannot be validated, don't return an error to allow another handle to validate it. var result = context.Options.JsonWebTokenHandler.ValidateToken(context.Token, parameters); @@ -490,8 +503,13 @@ namespace OpenIddict.Server // Store the token type (resolved from "typ" or "token_usage") as a special private claim. context.Principal.SetTokenType(result.TokenType switch { - JsonWebTokenTypes.AccessToken => TokenTypeHints.AccessToken, - JsonWebTokenTypes.IdentityToken => TokenTypeHints.IdToken, + var type when string.IsNullOrEmpty(type) => throw new InvalidOperationException("The token type cannot be resolved"), + + JsonWebTokenTypes.AccessToken => TokenTypeHints.AccessToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken => TokenTypeHints.AccessToken, + + JsonWebTokenTypes.IdentityToken => TokenTypeHints.IdToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken => TokenTypeHints.IdToken, JsonWebTokenTypes.Private.AuthorizationCode => TokenTypeHints.AuthorizationCode, JsonWebTokenTypes.Private.DeviceCode => TokenTypeHints.DeviceCode, diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 9e9a4512..2ed562d8 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -126,9 +126,8 @@ namespace OpenIddict.Server throw new SecurityTokenInvalidTypeException("The 'typ' header of the JWT token cannot be null or empty."); } - // If the generic type of the token is "JWT", try to resolve the actual type from the "token_usage" claim. - if (string.Equals(type, JwtConstants.HeaderType, StringComparison.OrdinalIgnoreCase) && - ((JsonWebToken) token).TryGetPayloadValue(OpenIddictConstants.Claims.TokenUsage, out string usage)) + // If available, try to resolve the actual type from the "token_usage" claim. + if (((JsonWebToken) token).TryGetPayloadValue(OpenIddictConstants.Claims.TokenUsage, out string usage)) { type = usage switch { @@ -139,27 +138,21 @@ namespace OpenIddict.Server }; } + // Unlike IdentityModel, this custom validator deliberately uses case-insensitive comparisons. if (parameters.ValidTypes != null && parameters.ValidTypes.Any() && - !parameters.ValidTypes.Contains(type, StringComparer.Ordinal)) + !parameters.ValidTypes.Contains(type, StringComparer.OrdinalIgnoreCase)) { - throw new SecurityTokenInvalidTypeException("The type of the JWT token doesn't match the expected type."); + throw new SecurityTokenInvalidTypeException("The type of the JWT token doesn't match the expected type.") + { + InvalidType = type + }; } return type; }, // Note: audience and lifetime are manually validated by OpenIddict itself. ValidateAudience = 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 - } + ValidateLifetime = false }; /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index ae6f17a4..cef58b16 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -229,20 +229,21 @@ namespace OpenIddict.Validation parameters.IssuerSigningKeys = parameters.IssuerSigningKeys?.Concat(configuration.SigningKeys) ?? configuration.SigningKeys; - // If a specific token type is expected, override the default valid types to reject - // security tokens whose actual token type doesn't match the expected token type. - if (!string.IsNullOrEmpty(context.TokenType)) + parameters.ValidTypes = context.TokenType switch { - parameters.ValidTypes = new[] + // If no specific token type is expected, accept all token types at this stage. + // Additional filtering can be made based on the resolved/actual token type. + var type when string.IsNullOrEmpty(type) => Array.Empty(), + + // For access tokens, both "at+jwt" and "application/at+jwt" are valid. + TokenTypeHints.AccessToken => new[] { - context.TokenType switch - { - TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken, + JsonWebTokenTypes.AccessToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken + }, - _ => throw new InvalidOperationException("The token type is not supported.") - } - }; - } + _ => throw new InvalidOperationException("The token type is not supported.") + }; // If the token cannot be validated, don't return an error to allow another handle to validate it. var result = context.Options.JsonWebTokenHandler.ValidateToken(context.Token, parameters); @@ -266,7 +267,10 @@ namespace OpenIddict.Validation // Store the token type (resolved from "typ" or "token_usage") as a special private claim. context.Principal.SetTokenType(result.TokenType switch { - JsonWebTokenTypes.AccessToken => TokenTypeHints.AccessToken, + var type when string.IsNullOrEmpty(type) => throw new InvalidOperationException("The token type cannot be resolved"), + + JsonWebTokenTypes.AccessToken => TokenTypeHints.AccessToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken => TokenTypeHints.AccessToken, _ => throw new InvalidOperationException("The token type is not supported.") }); @@ -333,13 +337,14 @@ namespace OpenIddict.Validation try { - var principal = await _service.IntrospectTokenAsync(address, context.Token, TokenTypeHints.AccessToken) ?? + var principal = await _service.IntrospectTokenAsync(address, context.Token, context.TokenType) ?? throw new InvalidOperationException("An unknown error occurred while introspecting the access token."); - // Note: tokens that are considered valid at this point are assumed to be access tokens, + // Note: tokens that are considered valid at this point are assumed to be of the given type, // as the introspection handlers ensure the introspected token type matches the expected // type when a "token_usage" claim was returned as part of the introspection response. - context.Principal = principal.SetTokenType(TokenTypeHints.AccessToken); + // If no token type can be inferred, the token is assumed to be an access token. + context.Principal = principal.SetTokenType(context.TokenType ?? TokenTypeHints.AccessToken); context.Logger.LogTrace("The token '{Token}' was successfully introspected and the following claims " + "could be extracted: {Claims}.", context.Token, context.Principal.Claims); diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 45b068cb..6ecdf1f9 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -123,9 +123,8 @@ namespace OpenIddict.Validation throw new SecurityTokenInvalidTypeException("The 'typ' header of the JWT token cannot be null or empty."); } - // If the generic type of the token is "JWT", try to resolve the actual type from the "token_usage" claim. - if (string.Equals(type, JwtConstants.HeaderType, StringComparison.OrdinalIgnoreCase) && - ((JsonWebToken) token).TryGetPayloadValue(Claims.TokenUsage, out string usage)) + // If available, try to resolve the actual type from the "token_usage" claim. + if (((JsonWebToken) token).TryGetPayloadValue(Claims.TokenUsage, out string usage)) { type = usage switch { @@ -136,18 +135,21 @@ namespace OpenIddict.Validation }; } + // Unlike IdentityModel, this custom validator deliberately uses case-insensitive comparisons. if (parameters.ValidTypes != null && parameters.ValidTypes.Any() && - !parameters.ValidTypes.Contains(type, StringComparer.Ordinal)) + !parameters.ValidTypes.Contains(type, StringComparer.OrdinalIgnoreCase)) { - throw new SecurityTokenInvalidTypeException("The type of the JWT token doesn't match the expected type."); + throw new SecurityTokenInvalidTypeException("The type of the JWT token doesn't match the expected type.") + { + InvalidType = type + }; } return type; }, // Note: audience and lifetime are manually validated by OpenIddict itself. ValidateAudience = false, - ValidateLifetime = false, - ValidTypes = new[] { JsonWebTokenTypes.AccessToken } + ValidateLifetime = false }; } }