Skip to content

Commit

Permalink
Abort sign-in demands whose principal contains a standard claim with …
Browse files Browse the repository at this point in the history
…an invalid claim value type
  • Loading branch information
kevinchalet committed Jan 17, 2024
1 parent e27e42a commit cf3e960
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 94 deletions.
9 changes: 9 additions & 0 deletions src/OpenIddict.Abstractions/OpenIddictResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1578,6 +1578,15 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0423" xml:space="preserve">
<value>Multiple claims of the same type are present in the identity or principal.</value>
</data>
<data name="ID0424" xml:space="preserve">
<value>The '{0}' claim present in the specified principal is malformed or isn't of the expected type.</value>
</data>
<data name="ID0425" xml:space="preserve">
<value>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'.</value>
</data>
<data name="ID0426" xml:space="preserve">
<value>The specified principal contains a subject claim, which is not valid for this operation.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
Expand Down
187 changes: 119 additions & 68 deletions src/OpenIddict.Client/OpenIddictClientHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ GrantTypes.DeviceCode or GrantTypes.Implicit or
}

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 ?
Expand Down Expand Up @@ -1618,14 +1618,10 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
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),
Expand Down Expand Up @@ -1696,28 +1692,22 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)

return default;

static bool ValidateClaimGroup(KeyValuePair<string, List<Claim>> claims) => claims switch
static bool ValidateClaimGroup(string name, List<Claim> 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<Claim> 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<Claim> 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<Claim> 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
Expand Down Expand Up @@ -2954,14 +2944,10 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
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),
Expand Down Expand Up @@ -3032,28 +3018,22 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)

return default;

static bool ValidateClaimGroup(KeyValuePair<string, List<Claim>> claims) => claims switch
static bool ValidateClaimGroup(string name, List<Claim> 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<Claim> 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<Claim> 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<Claim> 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
Expand Down Expand Up @@ -3871,14 +3851,10 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
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),
Expand All @@ -3889,13 +3865,10 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)

return default;

static bool ValidateClaimGroup(KeyValuePair<string, List<Claim>> claims) => claims switch
static bool ValidateClaimGroup(string name, List<Claim> values) => name switch
{
// The following JWT claims MUST be represented as unique strings.
{
Key: Claims.Subject,
Value: List<Claim> 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
Expand Down Expand Up @@ -4191,14 +4164,53 @@ public ValueTask HandleAsync(ProcessChallengeContext context)
}

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 ?
new InvalidOperationException(SR.GetResourceString(SR.ID0304)) :
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<Claim> 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;
}
}
Expand Down Expand Up @@ -5842,14 +5854,53 @@ public ValueTask HandleAsync(ProcessSignOutContext context)
}

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 ?
new InvalidOperationException(SR.GetResourceString(SR.ID0304)) :
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<Claim> 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;
}
}
Expand Down
Loading

0 comments on commit cf3e960

Please sign in to comment.