Browse Source

Abort sign-in demands whose principal contains a standard claim with an invalid claim value type

pull/1963/head
Kévin Chalet 2 years ago
parent
commit
cf3e960055
  1. 9
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 187
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  3. 81
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  4. 34
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

9
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -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>

187
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<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
@ -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<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
@ -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<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
@ -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<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;
}
}
@ -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<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;
}
}

81
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<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.AuthorizedParty or Claims.Issuer or Claims.JwtId 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.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<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 => 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<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.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<Claim> 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
};
}
}

34
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<HandleAuthorizationRequestContext>(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<InvalidOperationException>(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()
{

Loading…
Cancel
Save