Browse Source

Support the long application/at+jwt and application/jwt forms

pull/998/head
Kévin Chalet 6 years ago
parent
commit
dad788a936
  1. 5
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  2. 56
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  3. 25
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  4. 35
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  5. 16
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs

5
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";

56
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<string>(),
// 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,

25
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
};
/// <summary>

35
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<string>(),
// 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);

16
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
};
}
}

Loading…
Cancel
Save