diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 30ec5114..9503c937 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1578,6 +1578,15 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
Multiple claims of the same type are present in the identity or principal.
+
+ The '{0}' claim present in the specified principal is malformed or isn't of the expected type.
+
+
+ The specified principal contains an authenticated identity, which is not valid for this operation. Make sure that 'ClaimsPrincipal.Identity.AuthenticationType' is null and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'false'.
+
+
+ The specified principal contains a subject claim, which is not valid for this operation.
+
The security token is missing.
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index cc0df1b7..4174cc69 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
@@ -332,7 +332,7 @@ public static partial class OpenIddictClientHandlers
}
if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) &&
- context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
+ context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
context.Options.Registrations.Count is not 1)
{
throw context.Options.Registrations.Count is 0 ?
@@ -1618,14 +1618,10 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
foreach (var group in context.FrontchannelIdentityTokenPrincipal.Claims
- .GroupBy(claim => claim.Type)
- .ToDictionary(group => group.Key, group => group.ToList()))
+ .GroupBy(static claim => claim.Type)
+ .ToDictionary(static group => group.Key, group => group.ToList())
+ .Where(static group => !ValidateClaimGroup(group.Key, group.Value)))
{
- if (ValidateClaimGroup(group))
- {
- continue;
- }
-
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2121(group.Key),
@@ -1696,28 +1692,22 @@ public static partial class OpenIddictClientHandlers
return default;
- static bool ValidateClaimGroup(KeyValuePair> claims) => claims switch
+ static bool ValidateClaimGroup(string name, List values) => name switch
{
- // The following JWT claims MUST be represented as unique strings.
- {
- Key: Claims.AuthenticationContextReference or Claims.AuthorizedParty or
- Claims.Issuer or Claims.Nonce or Claims.Subject,
- Value: List values
- } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.String,
+ // The following claims MUST be represented as unique strings.
+ Claims.AuthenticationContextReference or Claims.AuthorizedParty or
+ Claims.Issuer or Claims.Nonce or Claims.Subject
+ => values is [{ ValueType: ClaimValueTypes.String }],
- // The following JWT claims MUST be represented as unique strings or array of strings.
- {
- Key: Claims.Audience or Claims.AuthenticationMethodReference,
- Value: List values
- } => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ // The following claims MUST be represented as unique strings or array of strings.
+ Claims.Audience or Claims.AuthenticationMethodReference
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
- // The following JWT claims MUST be represented as unique numeric dates.
- {
- Key: Claims.AuthenticationTime or Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore,
- Value: List values
- } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
- ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
- ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64,
+ // The following claims MUST be represented as unique numeric dates.
+ Claims.AuthenticationTime or Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
+ => values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
+ ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
+ ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64 }],
// Claims that are not in the well-known list can be of any type.
_ => true
@@ -2954,14 +2944,10 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
foreach (var group in context.BackchannelIdentityTokenPrincipal.Claims
- .GroupBy(claim => claim.Type)
- .ToDictionary(group => group.Key, group => group.ToList()))
+ .GroupBy(static claim => claim.Type)
+ .ToDictionary(static group => group.Key, group => group.ToList())
+ .Where(static group => !ValidateClaimGroup(group.Key, group.Value)))
{
- if (ValidateClaimGroup(group))
- {
- continue;
- }
-
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2125(group.Key),
@@ -3032,28 +3018,22 @@ public static partial class OpenIddictClientHandlers
return default;
- static bool ValidateClaimGroup(KeyValuePair> claims) => claims switch
+ static bool ValidateClaimGroup(string name, List values) => name switch
{
- // The following JWT claims MUST be represented as unique strings.
- {
- Key: Claims.AuthenticationContextReference or Claims.AuthorizedParty or
- Claims.Issuer or Claims.Nonce or Claims.Subject,
- Value: List values
- } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.String,
+ // The following claims MUST be represented as unique strings.
+ Claims.AuthenticationContextReference or Claims.AuthorizedParty or
+ Claims.Issuer or Claims.Nonce or Claims.Subject
+ => values is [{ ValueType: ClaimValueTypes.String }],
- // The following JWT claims MUST be represented as unique strings or array of strings.
- {
- Key: Claims.Audience or Claims.AuthenticationMethodReference,
- Value: List values
- } => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ // The following claims MUST be represented as unique strings or array of strings.
+ Claims.Audience or Claims.AuthenticationMethodReference
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
- // The following JWT claims MUST be represented as unique numeric dates.
- {
- Key: Claims.AuthenticationTime or Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore,
- Value: List values
- } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
- ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
- ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64,
+ // The following claims MUST be represented as unique numeric dates.
+ Claims.AuthenticationTime or Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
+ => values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
+ ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
+ ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64 }],
// Claims that are not in the well-known list can be of any type.
_ => true
@@ -3871,14 +3851,10 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
foreach (var group in context.UserinfoTokenPrincipal.Claims
- .GroupBy(claim => claim.Type)
- .ToDictionary(group => group.Key, group => group.ToList()))
+ .GroupBy(static claim => claim.Type)
+ .ToDictionary(static group => group.Key, group => group.ToList())
+ .Where(static group => !ValidateClaimGroup(group.Key, group.Value)))
{
- if (ValidateClaimGroup(group))
- {
- continue;
- }
-
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2131(group.Key),
@@ -3889,13 +3865,10 @@ public static partial class OpenIddictClientHandlers
return default;
- static bool ValidateClaimGroup(KeyValuePair> claims) => claims switch
+ static bool ValidateClaimGroup(string name, List values) => name switch
{
- // The following JWT claims MUST be represented as unique strings.
- {
- Key: Claims.Subject,
- Value: List values
- } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.String,
+ // The following claims MUST be represented as unique strings.
+ Claims.Subject => values is [{ ValueType: ClaimValueTypes.String }],
// Claims that are not in the well-known list can be of any type.
_ => true
@@ -4191,7 +4164,7 @@ public static partial class OpenIddictClientHandlers
}
if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) &&
- context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
+ context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
context.Options.Registrations.Count is not 1)
{
throw context.Options.Registrations.Count is 0 ?
@@ -4199,6 +4172,45 @@ public static partial class OpenIddictClientHandlers
new InvalidOperationException(SR.GetResourceString(SR.ID0305));
}
+ if (context.Principal is not { Identity: ClaimsIdentity })
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0011));
+ }
+
+ if (context.Principal.Identity.IsAuthenticated)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0425));
+ }
+
+ if (context.Principal.HasClaim(Claims.Subject))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0426));
+ }
+
+ foreach (var group in context.Principal.Claims
+ .GroupBy(static claim => claim.Type)
+ .ToDictionary(static group => group.Key, static group => group.ToList())
+ .Where(static group => !ValidateClaimGroup(group.Key, group.Value)))
+ {
+ throw new InvalidOperationException(SR.FormatID0424(group.Key));
+ }
+
+ static bool ValidateClaimGroup(string name, List values) => name switch
+ {
+ // The following claims MUST be represented as unique strings or array of strings.
+ Claims.Private.Audience or Claims.Private.Resource or Claims.Private.Presenter
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+
+ // The following claims MUST be represented as unique integers.
+ Claims.Private.StateTokenLifetime
+ => values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
+ ClaimValueTypes.Integer64 or ClaimValueTypes.UInteger32 or
+ ClaimValueTypes.UInteger64 }],
+
+ // Claims that are not in the well-known list can be of any type.
+ _ => true
+ };
+
return default;
}
}
@@ -5842,7 +5854,7 @@ public static partial class OpenIddictClientHandlers
}
if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) &&
- context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
+ context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
context.Options.Registrations.Count is not 1)
{
throw context.Options.Registrations.Count is 0 ?
@@ -5850,6 +5862,45 @@ public static partial class OpenIddictClientHandlers
new InvalidOperationException(SR.GetResourceString(SR.ID0341));
}
+ if (context.Principal is not { Identity: ClaimsIdentity })
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0011));
+ }
+
+ if (context.Principal.Identity.IsAuthenticated)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0425));
+ }
+
+ if (context.Principal.HasClaim(Claims.Subject))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0426));
+ }
+
+ foreach (var group in context.Principal.Claims
+ .GroupBy(static claim => claim.Type)
+ .ToDictionary(static group => group.Key, static group => group.ToList())
+ .Where(static group => !ValidateClaimGroup(group.Key, group.Value)))
+ {
+ throw new InvalidOperationException(SR.FormatID0424(group.Key));
+ }
+
+ static bool ValidateClaimGroup(string name, List values) => name switch
+ {
+ // The following claims MUST be represented as unique strings or array of strings.
+ Claims.Private.Audience or Claims.Private.Resource or Claims.Private.Presenter
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+
+ // The following claims MUST be represented as unique integers.
+ Claims.Private.StateTokenLifetime
+ => values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
+ ClaimValueTypes.Integer64 or ClaimValueTypes.UInteger32 or
+ ClaimValueTypes.UInteger64 }],
+
+ // Claims that are not in the well-known list can be of any type.
+ _ => true
+ };
+
return default;
}
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index d8afa149..98971db6 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
@@ -623,14 +623,10 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(context.ClientAssertionPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
foreach (var group in context.ClientAssertionPrincipal.Claims
- .GroupBy(claim => claim.Type)
- .ToDictionary(group => group.Key, group => group.ToList()))
+ .GroupBy(static claim => claim.Type)
+ .ToDictionary(static group => group.Key, group => group.ToList())
+ .Where(static group => !ValidateClaimGroup(group.Key, group.Value)))
{
- if (ValidateClaimGroup(group))
- {
- continue;
- }
-
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2171(group.Key),
@@ -706,27 +702,20 @@ public static partial class OpenIddictServerHandlers
return default;
- static bool ValidateClaimGroup(KeyValuePair> claims) => claims switch
+ static bool ValidateClaimGroup(string name, List values) => name switch
{
- // The following JWT claims MUST be represented as unique strings.
- {
- Key: Claims.AuthorizedParty or Claims.Issuer or Claims.JwtId or Claims.Subject,
- Value: List values
- } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.String,
+ // The following claims MUST be represented as unique strings.
+ Claims.AuthorizedParty or Claims.Issuer or Claims.JwtId or Claims.Subject
+ => values is [{ ValueType: ClaimValueTypes.String }],
- // The following JWT claims MUST be represented as unique strings or array of strings.
- {
- Key: Claims.Audience,
- Value: List values
- } => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ // The following claims MUST be represented as unique strings or array of strings.
+ Claims.Audience => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
- // The following JWT claims MUST be represented as unique numeric dates.
- {
- Key: Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore,
- Value: List values
- } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
- ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
- ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64,
+ // The following claims MUST be represented as unique numeric dates.
+ Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
+ => values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
+ ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
+ ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64 }],
// Claims that are not in the well-known list can be of any type.
_ => true
@@ -2123,7 +2112,7 @@ public static partial class OpenIddictServerHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0012));
}
- if (!string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject)))
+ if (context.Principal.HasClaim(Claims.Subject))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0013));
}
@@ -2142,7 +2131,47 @@ public static partial class OpenIddictServerHandlers
}
}
+ foreach (var group in context.Principal.Claims
+ .GroupBy(static claim => claim.Type)
+ .ToDictionary(static group => group.Key, static group => group.ToList())
+ .Where(static group => !ValidateClaimGroup(group.Key, group.Value)))
+ {
+ throw new InvalidOperationException(SR.FormatID0424(group.Key));
+ }
+
return default;
+
+ static bool ValidateClaimGroup(string name, List values) => name switch
+ {
+ // The following claims MUST be represented as unique strings.
+ Claims.AuthenticationContextReference or Claims.Subject or
+ Claims.Private.AuthorizationId or Claims.Private.CreationDate or
+ Claims.Private.DeviceCodeId or Claims.Private.ExpirationDate or
+ Claims.Private.TokenId
+ => values is [{ ValueType: ClaimValueTypes.String }],
+
+ // The following claims MUST be represented as unique strings or array of strings.
+ Claims.AuthenticationMethodReference or Claims.Private.Audience or
+ Claims.Private.Presenter or Claims.Private.Resource
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+
+ // The following claims MUST be represented as unique integers.
+ Claims.Private.AccessTokenLifetime or Claims.Private.AuthorizationCodeLifetime or
+ Claims.Private.DeviceCodeLifetime or Claims.Private.IdentityTokenLifetime or
+ Claims.Private.RefreshTokenLifetime or Claims.Private.RefreshTokenLifetime
+ => values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
+ ClaimValueTypes.Integer64 or ClaimValueTypes.UInteger32 or
+ ClaimValueTypes.UInteger64 }],
+
+ // The following claims MUST be represented as unique numeric dates.
+ Claims.AuthenticationTime
+ => values is [{ ValueType: ClaimValueTypes.Integer or ClaimValueTypes.Integer32 or
+ ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
+ ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64 }],
+
+ // Claims that are not in the well-known list can be of any type.
+ _ => true
+ };
}
}
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
index dcc2bc3d..d2747fc8 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
@@ -1449,6 +1449,40 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.GetResourceString(SR.ID0013), exception.Message);
}
+ [Fact]
+ public async Task ProcessSignIn_InvalidClaimValueTypeCausesAnException()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
+ .SetClaim(Claims.Subject, 42);
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act and assert
+ var exception = await Assert.ThrowsAsync(delegate
+ {
+ return client.PostAsync("/connect/authorize", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ RedirectUri = "http://www.fabrikam.com/path",
+ ResponseType = ResponseTypes.Code
+ });
+ });
+
+ Assert.Equal(SR.FormatID0424(Claims.Subject), exception.Message);
+ }
+
[Fact]
public async Task ProcessSignIn_ScopeDefaultsToOpenId()
{