Browse Source

Leverage IdentityModel's new TypeValidator mechanism to allow validating JWT tokens issued by OpenIddict 1.x/2.x

pull/947/head
Kévin Chalet 6 years ago
parent
commit
be23e18023
  1. 2
      eng/Versions.props
  2. 7
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
  3. 25
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
  4. 25
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
  5. 7
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
  6. 25
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
  7. 25
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
  8. 8
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  9. 33
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  10. 33
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  11. 2
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

2
eng/Versions.props

@ -36,7 +36,7 @@
<DataAnnotationsVersion>4.7.0</DataAnnotationsVersion>
<EntityFrameworkVersion>6.4.0</EntityFrameworkVersion>
<JetBrainsVersion>2019.1.3</JetBrainsVersion>
<IdentityModelVersion>6.5.0</IdentityModelVersion>
<IdentityModelVersion>6.6.0</IdentityModelVersion>
<ImmutableCollectionsVersion>1.7.0</ImmutableCollectionsVersion>
<LinqAsyncVersion>4.1.1</LinqAsyncVersion>
<MongoDbVersion>2.9.0</MongoDbVersion>

7
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs

@ -19,8 +19,11 @@ namespace OpenIddict.Server.AspNetCore
public static class JsonWebTokenTypes
{
public const string AuthorizationRequest = "oi_auth_req";
public const string LogoutRequest = "oi_lgt_req";
public static class Private
{
public const string AuthorizationRequest = "oi_authrq+jwt";
public const string LogoutRequest = "oi_lgtrq+jwt";
}
}
public static class Properties

25
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs

@ -127,16 +127,12 @@ namespace OpenIddict.Server.AspNetCore
return;
}
// Restore the authorization request parameters from the serialized payload.
var parameters = new TokenValidationParameters
{
IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key),
ValidateLifetime = false,
ValidAudience = context.Issuer.AbsoluteUri,
ValidIssuer = context.Issuer.AbsoluteUri,
ValidTypes = new[] { JsonWebTokenTypes.AuthorizationRequest }
};
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key);
parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key);
parameters.ValidAudience = context.Issuer?.AbsoluteUri;
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.AuthorizationRequest };
var result = context.Options.JsonWebTokenHandler.ValidateToken(token, parameters);
if (!result.IsValid)
@ -158,6 +154,7 @@ namespace OpenIddict.Server.AspNetCore
throw new InvalidOperationException("The authorization request payload is malformed.");
}
// Restore the authorization request parameters from the serialized payload.
foreach (var parameter in document.RootElement.EnumerateObject())
{
// Avoid overriding the current request parameters.
@ -253,13 +250,13 @@ namespace OpenIddict.Server.AspNetCore
{
AdditionalHeaderClaims = new Dictionary<string, object>(StringComparer.Ordinal)
{
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AuthorizationRequest
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationRequest
},
Audience = context.Issuer.AbsoluteUri,
Audience = context.Issuer?.AbsoluteUri,
Claims = context.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => parameter.Value.Value),
Issuer = context.Issuer.AbsoluteUri,
Issuer = context.Issuer?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity()
});
@ -268,7 +265,7 @@ namespace OpenIddict.Server.AspNetCore
encryptingCredentials: context.Options.EncryptionCredentials.First(),
additionalHeaderClaims: new Dictionary<string, object>
{
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AuthorizationRequest
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationRequest
});
// Note: the cache key is always prefixed with a specific marker

25
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs

@ -125,16 +125,12 @@ namespace OpenIddict.Server.AspNetCore
return;
}
// Restore the authorization request parameters from the serialized payload.
var parameters = new TokenValidationParameters
{
IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key),
ValidateLifetime = false,
ValidAudience = context.Issuer.AbsoluteUri,
ValidIssuer = context.Issuer.AbsoluteUri,
ValidTypes = new[] { JsonWebTokenTypes.LogoutRequest }
};
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key);
parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key);
parameters.ValidAudience = context.Issuer?.AbsoluteUri;
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.LogoutRequest };
var result = context.Options.JsonWebTokenHandler.ValidateToken(token, parameters);
if (!result.IsValid)
@ -156,6 +152,7 @@ namespace OpenIddict.Server.AspNetCore
throw new InvalidOperationException("The logout request payload is malformed.");
}
// Restore the authorization request parameters from the serialized payload.
foreach (var parameter in document.RootElement.EnumerateObject())
{
// Avoid overriding the current request parameters.
@ -251,13 +248,13 @@ namespace OpenIddict.Server.AspNetCore
{
AdditionalHeaderClaims = new Dictionary<string, object>(StringComparer.Ordinal)
{
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.LogoutRequest
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.LogoutRequest
},
Audience = context.Issuer.AbsoluteUri,
Audience = context.Issuer?.AbsoluteUri,
Claims = context.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => parameter.Value.Value),
Issuer = context.Issuer.AbsoluteUri,
Issuer = context.Issuer?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity()
});
@ -266,7 +263,7 @@ namespace OpenIddict.Server.AspNetCore
encryptingCredentials: context.Options.EncryptionCredentials.First(),
additionalHeaderClaims: new Dictionary<string, object>
{
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AuthorizationRequest
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationRequest
});
// Note: the cache key is always prefixed with a specific marker

7
src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs

@ -19,8 +19,11 @@ namespace OpenIddict.Server.Owin
public static class JsonWebTokenTypes
{
public const string AuthorizationRequest = "oi_auth_req";
public const string LogoutRequest = "oi_lgt_req";
public static class Private
{
public const string AuthorizationRequest = "oi_authrq+jwt";
public const string LogoutRequest = "oi_lgtrq+jwt";
}
}
public static class Properties

25
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs

@ -126,16 +126,12 @@ namespace OpenIddict.Server.Owin
return;
}
// Restore the authorization request parameters from the serialized payload.
var parameters = new TokenValidationParameters
{
IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key),
ValidateLifetime = false,
ValidAudience = context.Issuer.AbsoluteUri,
ValidIssuer = context.Issuer.AbsoluteUri,
ValidTypes = new[] { JsonWebTokenTypes.AuthorizationRequest }
};
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key);
parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key);
parameters.ValidAudience = context.Issuer?.AbsoluteUri;
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.AuthorizationRequest };
var result = context.Options.JsonWebTokenHandler.ValidateToken(token, parameters);
if (!result.IsValid)
@ -157,6 +153,7 @@ namespace OpenIddict.Server.Owin
throw new InvalidOperationException("The authorization request payload is malformed.");
}
// Restore the authorization request parameters from the serialized payload.
foreach (var parameter in document.RootElement.EnumerateObject())
{
// Avoid overriding the current request parameters.
@ -247,13 +244,13 @@ namespace OpenIddict.Server.Owin
{
AdditionalHeaderClaims = new Dictionary<string, object>(StringComparer.Ordinal)
{
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AuthorizationRequest
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationRequest
},
Audience = context.Issuer.AbsoluteUri,
Audience = context.Issuer?.AbsoluteUri,
Claims = context.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => parameter.Value.Value),
Issuer = context.Issuer.AbsoluteUri,
Issuer = context.Issuer?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity()
});
@ -262,7 +259,7 @@ namespace OpenIddict.Server.Owin
encryptingCredentials: context.Options.EncryptionCredentials.First(),
additionalHeaderClaims: new Dictionary<string, object>
{
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AuthorizationRequest
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationRequest
});
// Note: the cache key is always prefixed with a specific marker

25
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs

@ -124,16 +124,12 @@ namespace OpenIddict.Server.Owin
return;
}
// Restore the authorization request parameters from the serialized payload.
var parameters = new TokenValidationParameters
{
IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key),
TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key),
ValidateLifetime = false,
ValidAudience = context.Issuer.AbsoluteUri,
ValidIssuer = context.Issuer.AbsoluteUri,
ValidTypes = new[] { JsonWebTokenTypes.LogoutRequest }
};
var parameters = context.Options.TokenValidationParameters.Clone();
parameters.IssuerSigningKeys = context.Options.SigningCredentials.Select(credentials => credentials.Key);
parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key);
parameters.ValidAudience = context.Issuer?.AbsoluteUri;
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.LogoutRequest };
var result = context.Options.JsonWebTokenHandler.ValidateToken(token, parameters);
if (!result.IsValid)
@ -155,6 +151,7 @@ namespace OpenIddict.Server.Owin
throw new InvalidOperationException("The logout request payload is malformed.");
}
// Restore the authorization request parameters from the serialized payload
foreach (var parameter in document.RootElement.EnumerateObject())
{
// Avoid overriding the current request parameters.
@ -245,13 +242,13 @@ namespace OpenIddict.Server.Owin
{
AdditionalHeaderClaims = new Dictionary<string, object>(StringComparer.Ordinal)
{
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.LogoutRequest
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.LogoutRequest
},
Audience = context.Issuer.AbsoluteUri,
Audience = context.Issuer?.AbsoluteUri,
Claims = context.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => parameter.Value.Value),
Issuer = context.Issuer.AbsoluteUri,
Issuer = context.Issuer?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity()
});
@ -260,7 +257,7 @@ namespace OpenIddict.Server.Owin
encryptingCredentials: context.Options.EncryptionCredentials.First(),
additionalHeaderClaims: new Dictionary<string, object>
{
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.LogoutRequest
[JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.LogoutRequest
});
// Note: the cache key is always prefixed with a specific marker

8
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -445,7 +445,7 @@ namespace OpenIddict.Server
parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key);
// If a specific token type is expected, override the default valid types to reject
// security tokens whose "typ" header doesn't match the expected token type.
// security tokens whose actual token type doesn't match the expected token type.
if (!string.IsNullOrEmpty(context.TokenType))
{
parameters.ValidTypes = new[]
@ -484,8 +484,8 @@ namespace OpenIddict.Server
// Attach the principal extracted from the token to the parent event context.
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity);
// Store the token type as a special private claim.
context.Principal.SetTokenType(token.Typ switch
// 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,
@ -715,7 +715,7 @@ namespace OpenIddict.Server
if (!string.Equals(type, context.TokenType, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(new StringBuilder()
.AppendFormat("The type of token associated with the deserialized principal ({0})", type)
.AppendFormat("The type of token associated with the deserialized principal ({0}) ", type)
.AppendFormat("doesn't match the expected token type ({0}).", context.TokenType)
.ToString());
}

33
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
@ -113,6 +114,38 @@ namespace OpenIddict.Server
ClockSkew = TimeSpan.Zero,
NameClaimType = OpenIddictConstants.Claims.Name,
RoleClaimType = OpenIddictConstants.Claims.Role,
// In previous versions of OpenIddict (1.x and 2.x), all the JWT tokens (access and identity tokens)
// were issued with the generic "typ": "JWT" header. To prevent confused deputy and token substitution
// attacks, a special "token_usage" claim was added to the JWT payload to convey the actual token type.
// This validator overrides the default logic used by IdentityModel to resolve the type from this claim.
TypeValidator = (type, token, parameters) =>
{
if (string.IsNullOrEmpty(type))
{
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))
{
type = usage switch
{
TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken,
TokenTypeHints.IdToken => JsonWebTokenTypes.IdentityToken,
_ => throw new NotSupportedException("The token usage of the JWT token is not supported.")
};
}
if (parameters.ValidTypes != null && parameters.ValidTypes.Any() &&
!parameters.ValidTypes.Contains(type, StringComparer.Ordinal))
{
throw new SecurityTokenInvalidTypeException("The type of the JWT token doesn't match the expected type.");
}
return type;
},
// Note: audience and lifetime are manually validated by OpenIddict itself.
ValidateAudience = false,
ValidateLifetime = false,

33
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using static OpenIddict.Abstractions.OpenIddictConstants;
@ -92,6 +93,38 @@ namespace OpenIddict.Validation
ClockSkew = TimeSpan.Zero,
NameClaimType = Claims.Name,
RoleClaimType = Claims.Role,
// In previous versions of OpenIddict (1.x and 2.x), all the JWT tokens (access and identity tokens)
// were issued with the generic "typ": "JWT" header. To prevent confused deputy and token substitution
// attacks, a special "token_usage" claim was added to the JWT payload to convey the actual token type.
// This validator overrides the default logic used by IdentityModel to resolve the type from this claim.
TypeValidator = (type, token, parameters) =>
{
if (string.IsNullOrEmpty(type))
{
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))
{
type = usage switch
{
TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken,
TokenTypeHints.IdToken => JsonWebTokenTypes.IdentityToken,
_ => throw new NotSupportedException("The token usage of the JWT token is not supported.")
};
}
if (parameters.ValidTypes != null && parameters.ValidTypes.Any() &&
!parameters.ValidTypes.Contains(type, StringComparer.Ordinal))
{
throw new SecurityTokenInvalidTypeException("The type of the JWT token doesn't match the expected type.");
}
return type;
},
// Note: audience and lifetime are manually validated by OpenIddict itself.
ValidateAudience = false,
ValidateLifetime = false,

2
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -459,7 +459,7 @@ namespace OpenIddict.Server.FunctionalTests
// Assert
Assert.Equal(new StringBuilder()
.AppendFormat("The type of token associated with the deserialized principal ({0})", TokenTypeHints.AuthorizationCode)
.AppendFormat("The type of token associated with the deserialized principal ({0}) ", TokenTypeHints.AuthorizationCode)
.AppendFormat("doesn't match the expected token type ({0}).", TokenTypeHints.AccessToken)
.ToString(), exception.Message);
}

Loading…
Cancel
Save