diff --git a/eng/Versions.props b/eng/Versions.props
index cef16f91..693acd6a 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -36,7 +36,7 @@
4.7.0
6.4.0
2019.1.3
- 6.5.0
+ 6.6.0
1.7.0
4.1.1
2.9.0
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
index dc2db292..5724b625 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
+++ b/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
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
index b1771dd3..5f62153e 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
+++ b/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(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
{
- [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AuthorizationRequest
+ [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationRequest
});
// Note: the cache key is always prefixed with a specific marker
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
index ac47f4cf..c7ad5e58 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
+++ b/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(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
{
- [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AuthorizationRequest
+ [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationRequest
});
// Note: the cache key is always prefixed with a specific marker
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
index f4ac8c25..b3a4956e 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
+++ b/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
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
index bbd87364..1ee04b47 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
+++ b/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(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
{
- [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AuthorizationRequest
+ [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationRequest
});
// Note: the cache key is always prefixed with a specific marker
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
index 7192b2cc..bbe59ba4 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
+++ b/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(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
{
- [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.LogoutRequest
+ [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.LogoutRequest
});
// Note: the cache key is always prefixed with a specific marker
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index 4d17d3c5..7ee0f0a5 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/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());
}
diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs
index 480e2ec3..a36567d5 100644
--- a/src/OpenIddict.Server/OpenIddictServerOptions.cs
+++ b/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,
diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs
index 8d14f410..6c10ecb9 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs
+++ b/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,
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
index b3260951..cc580065 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
+++ b/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);
}