diff --git a/Directory.Packages.props b/Directory.Packages.props index 0da8b6d25..102a04161 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + @@ -108,23 +108,23 @@ - + - + - + - + @@ -134,14 +134,14 @@ - - - - - - + + + + + + diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/DPoPOptions.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/DPoPOptions.cs index 0a79904d1..73581047f 100644 --- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/DPoPOptions.cs +++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/DPoPOptions.cs @@ -27,6 +27,22 @@ public class DPoPOptions /// The allowed signing algorithms used in validating DPoP proof tokens. Defaults to: /// RSA256, RSA384, RSA512, PS256, PS384, PS512, ES256, ES384, ES512. /// + /// + /// + /// + /// Specifies the allowed signature algorithms for DPoP proof tokens. The "alg" headers of proofs + /// are validated against this collection, and the dpop_signing_alg_values_supported discovery property is populated + /// with these values. + /// + /// + /// Defaults to [RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512], which allows the RSA, Probabilistic + /// RSA, or ECDSA signing algorithms with 256, 384, or 512-bit SHA hashing. + /// + /// + /// If set to an empty collection, all algorithms (including symmetric algorithms) are allowed, and the + /// dpop_signing_alg_values_supported will not be set. Explicitly listing the expected values is recommended. + /// + /// public ICollection SupportedDPoPSigningAlgorithms { get; set; } = [ SecurityAlgorithms.RsaSha256, diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs index 9801927a1..c09d463d6 100644 --- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs +++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/IdentityServerOptions.cs @@ -6,6 +6,7 @@ using Duende.IdentityServer.Stores.Serialization; using Duende.IdentityServer.Validation; +using Microsoft.IdentityModel.Tokens; namespace Duende.IdentityServer.Configuration; @@ -205,27 +206,86 @@ public class IdentityServerOptions public PushedAuthorizationOptions PushedAuthorization { get; set; } = new PushedAuthorizationOptions(); /// - /// The allowed clock skew for JWT lifetime validation. Except for DPoP proofs, - /// all JWTs that have their lifetime validated use this setting to control the - /// clock skew of lifetime validation. This includes JWT access tokens passed - /// to the user info, introspection, and local api endpoints, client - /// authentication JWTs used in private_key_jwt authentication, JWT secured - /// authorization requests (JAR), and custom usage of the - /// , such as in a token exchange implementation. + /// The allowed clock skew for JWT lifetime validation. This setting controls the clock skew of lifetime validation + /// for all JWTs except DPoP proofs, including + /// + /// JWT access tokens passed to the user info, introspection, and local api endpoints + /// Authentication JWTs used in private_key_jwt authentication + /// JWT secured authorization requests (JAR request objects) + /// Custom usage of the , such as in a token exchange implementation. + /// + /// /// Defaults to five minutes. /// public TimeSpan JwtValidationClockSkew { get; set; } = TimeSpan.FromMinutes(5); /// - /// The allowed algorithms for JWT validation. Except for DPoP proofs, all JWTs validated by IdentityServer use this - /// setting to control the allowed signing algorithms. This includes JWT - /// access tokens passed to the user info, introspection, and local api endpoints, - /// client authentication JWTs used in private_key_jwt authentication, JWT secured - /// authorization requests (JAR), and custom usage of the , - /// such as in a token exchange implementation. Defaults to an empty collection which - /// allows all algorithms. - /// - public ICollection AllowedJwtAlgorithms { get; set; } = []; + /// + /// Specifies the allowed signature algorithms for JWT secured authorization requests (JAR). The "alg" header of JAR + /// request objects is validated against this collection, and the + /// request_object_signing_alg_values_supported discovery property is populated with these values. + /// + /// + /// Defaults to [RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, HS256, HS384, HS512], which allows + /// the RSA, Probabilistic RSA, ECDSA, or HMAC signing algorithms with 256, 384, or 512-bit SHA hashing. + /// + /// + /// If set to an empty collection, all algorithms are allowed, but the request_object_signing_alg_values_supported + /// will not be set. Explicitly listing the expected values is recommended. + /// + /// + public ICollection SupportedRequestObjectSigningAlgorithms { get; set; } = + [ + SecurityAlgorithms.RsaSha256, + SecurityAlgorithms.RsaSha384, + SecurityAlgorithms.RsaSha512, + + SecurityAlgorithms.RsaSsaPssSha256, + SecurityAlgorithms.RsaSsaPssSha384, + SecurityAlgorithms.RsaSsaPssSha512, + + SecurityAlgorithms.EcdsaSha256, + SecurityAlgorithms.EcdsaSha384, + SecurityAlgorithms.EcdsaSha512, + + SecurityAlgorithms.HmacSha256, + SecurityAlgorithms.HmacSha384, + SecurityAlgorithms.HmacSha512 + ]; + + /// + /// + /// Specifies the allowed signature algorithms for client authentication using client assertions (the + /// private_key_jwt parameter). The "alg" header of client assertions is validated against this collection, and the + /// token_endpoint_auth_signing_alg_values_supported discovery property is populated with these values. + /// + /// + /// Defaults to [RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, HS256, HS384, HS512], which allows + /// the RSA, Probabilistic RSA, ECDSA, or HMAC signing algorithms with 256, 384, or 512-bit SHA hashing. + /// + /// + /// If set to an empty collection, all algorithms are allowed, but the + /// token_endpoint_auth_signing_alg_values_supported will not be set. Explicitly listing the expected values is + /// recommended. + /// + /// + public ICollection SupportedClientAssertionSigningAlgorithms { get; set; } = [ + SecurityAlgorithms.RsaSha256, + SecurityAlgorithms.RsaSha384, + SecurityAlgorithms.RsaSha512, + + SecurityAlgorithms.RsaSsaPssSha256, + SecurityAlgorithms.RsaSsaPssSha384, + SecurityAlgorithms.RsaSsaPssSha512, + + SecurityAlgorithms.EcdsaSha256, + SecurityAlgorithms.EcdsaSha384, + SecurityAlgorithms.EcdsaSha512, + + SecurityAlgorithms.HmacSha256, + SecurityAlgorithms.HmacSha384, + SecurityAlgorithms.HmacSha512 + ]; /// /// Gets or sets the options for enabling and configuring preview features in the server. diff --git a/identity-server/src/IdentityServer/ResponseHandling/Default/DiscoveryResponseGenerator.cs b/identity-server/src/IdentityServer/ResponseHandling/Default/DiscoveryResponseGenerator.cs index f959b60bf..377b5d371 100644 --- a/identity-server/src/IdentityServer/ResponseHandling/Default/DiscoveryResponseGenerator.cs +++ b/identity-server/src/IdentityServer/ResponseHandling/Default/DiscoveryResponseGenerator.cs @@ -326,8 +326,14 @@ where scope.ShowInDiscoveryDocument types.Add(OidcConstants.EndpointAuthenticationMethods.TlsClientAuth); types.Add(OidcConstants.EndpointAuthenticationMethods.SelfSignedTlsClientAuth); } - entries.Add(OidcConstants.Discovery.TokenEndpointAuthenticationMethodsSupported, types); + + if (types.Contains(OidcConstants.EndpointAuthenticationMethods.PrivateKeyJwt) && + !IEnumerableExtensions.IsNullOrEmpty(Options.SupportedClientAssertionSigningAlgorithms)) + { + entries.Add(OidcConstants.Discovery.TokenEndpointAuthSigningAlgorithmsSupported, + Options.SupportedClientAssertionSigningAlgorithms); + } } var signingCredentials = await Keys.GetAllSigningCredentialsAsync(); @@ -344,24 +350,11 @@ where scope.ShowInDiscoveryDocument { entries.Add(OidcConstants.Discovery.RequestParameterSupported, true); - entries.Add(OidcConstants.Discovery.RequestObjectSigningAlgorithmsSupported, new[] + if (!IEnumerableExtensions.IsNullOrEmpty(Options.SupportedRequestObjectSigningAlgorithms)) { - SecurityAlgorithms.RsaSha256, - SecurityAlgorithms.RsaSha384, - SecurityAlgorithms.RsaSha512, - - SecurityAlgorithms.RsaSsaPssSha256, - SecurityAlgorithms.RsaSsaPssSha384, - SecurityAlgorithms.RsaSsaPssSha512, - - SecurityAlgorithms.EcdsaSha256, - SecurityAlgorithms.EcdsaSha384, - SecurityAlgorithms.EcdsaSha512, - - SecurityAlgorithms.HmacSha256, - SecurityAlgorithms.HmacSha384, - SecurityAlgorithms.HmacSha512 - }); + entries.Add(OidcConstants.Discovery.RequestObjectSigningAlgorithmsSupported, + Options.SupportedRequestObjectSigningAlgorithms); + } if (Options.Endpoints.EnableJwtRequestUri) { @@ -388,7 +381,8 @@ where scope.ShowInDiscoveryDocument entries.Add(OidcConstants.Discovery.BackchannelUserCodeParameterSupported, true); } - if (Options.Endpoints.EnableTokenEndpoint) + if (Options.Endpoints.EnableTokenEndpoint && + !IEnumerableExtensions.IsNullOrEmpty(Options.DPoP.SupportedDPoPSigningAlgorithms)) { entries.Add(OidcConstants.Discovery.DPoPSigningAlgorithmsSupported, Options.DPoP.SupportedDPoPSigningAlgorithms); } diff --git a/identity-server/src/IdentityServer/Validation/Default/JwtRequestValidator.cs b/identity-server/src/IdentityServer/Validation/Default/JwtRequestValidator.cs index d823ccf96..8c4b1597d 100644 --- a/identity-server/src/IdentityServer/Validation/Default/JwtRequestValidator.cs +++ b/identity-server/src/IdentityServer/Validation/Default/JwtRequestValidator.cs @@ -173,7 +173,7 @@ protected virtual async Task ValidateJwtAsync(JwtRequestValidation RequireExpirationTime = true, ClockSkew = Options.JwtValidationClockSkew, - ValidAlgorithms = Options.AllowedJwtAlgorithms + ValidAlgorithms = Options.SupportedRequestObjectSigningAlgorithms }; var strictJarValidation = context.StrictJarValidation.HasValue ? context.StrictJarValidation.Value : Options.StrictJarValidation; diff --git a/identity-server/src/IdentityServer/Validation/Default/PrivateKeyJwtSecretValidator.cs b/identity-server/src/IdentityServer/Validation/Default/PrivateKeyJwtSecretValidator.cs index 3d3eba5b5..029fc1079 100644 --- a/identity-server/src/IdentityServer/Validation/Default/PrivateKeyJwtSecretValidator.cs +++ b/identity-server/src/IdentityServer/Validation/Default/PrivateKeyJwtSecretValidator.cs @@ -120,7 +120,7 @@ public async Task ValidateAsync(IEnumerable secr RequireExpirationTime = true, ClockSkew = _options.JwtValidationClockSkew, - ValidAlgorithms = _options.AllowedJwtAlgorithms + ValidAlgorithms = _options.SupportedClientAssertionSigningAlgorithms }; var issuer = await _issuerNameService.GetCurrentAsync(); @@ -128,7 +128,7 @@ public async Task ValidateAsync(IEnumerable secr if (enforceStrictAud) { // New strict audience validation requires that the audience be the issuer identifier, disallows multiple - // audiences in an array, and even disallows wrapping even a single audience in an array + // audiences in an array, and even disallows wrapping even a single audience in an array tokenValidationParameters.AudienceValidator = (audiences, token, parameters) => { // There isn't a particularly nice way to distinguish between a claim that is a single string wrapped in @@ -205,7 +205,7 @@ public async Task ValidateAsync(IEnumerable secr return success; } - // AudiencesMatch and AudiencesMatchIgnoringTrailingSlash are based on code from + // AudiencesMatch and AudiencesMatchIgnoringTrailingSlash are based on code from // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/bef98ca10ae55603ce6d37dfb7cd5af27791527c/src/Microsoft.IdentityModel.Tokens/Validators.cs#L158-L193 private bool AudiencesMatch(string tokenAudience, string validAudience) { diff --git a/identity-server/src/IdentityServer/Validation/Default/TokenValidator.cs b/identity-server/src/IdentityServer/Validation/Default/TokenValidator.cs index d4ef49681..c88f09677 100644 --- a/identity-server/src/IdentityServer/Validation/Default/TokenValidator.cs +++ b/identity-server/src/IdentityServer/Validation/Default/TokenValidator.cs @@ -279,8 +279,7 @@ private async Task ValidateJwtAsync(string jwtString, ValidIssuer = await _issuerNameService.GetCurrentAsync(), IssuerSigningKeys = validationKeys.Select(k => k.Key), ValidateLifetime = validateLifetime, - ClockSkew = _options.JwtValidationClockSkew, - ValidAlgorithms = _options.AllowedJwtAlgorithms + ClockSkew = _options.JwtValidationClockSkew }; if (audience.IsPresent()) diff --git a/identity-server/src/IdentityServer/Validation/Models/ValidatedRequest.cs b/identity-server/src/IdentityServer/Validation/Models/ValidatedRequest.cs index b48d00739..d69227ad0 100644 --- a/identity-server/src/IdentityServer/Validation/Models/ValidatedRequest.cs +++ b/identity-server/src/IdentityServer/Validation/Models/ValidatedRequest.cs @@ -13,7 +13,7 @@ namespace Duende.IdentityServer.Validation; /// -/// Base class for a validate authorize or token request +/// Base class for a validated authorize or token request /// public class ValidatedRequest { diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs index 2533cb0a0..e815ee1d5 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/JwtRequestAuthorizeTests.cs @@ -913,7 +913,7 @@ public async Task authorize_should_reject_jwt_request_if_client_id_does_not_matc [Trait("Category", Category)] public async Task authorize_should_reject_jwt_request_if_signed_by_algorithm_not_allowed_by_configuration() { - _mockPipeline.Options.AllowedJwtAlgorithms = ["ES256"]; + _mockPipeline.Options.SupportedRequestObjectSigningAlgorithms = ["ES256"]; var requestJwt = CreateRequestJwt( issuer: _client.ClientId, audience: IdentityServerPipeline.BaseUrl, diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests.cs index 0f0b42dc6..a37aba995 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests.cs @@ -2,7 +2,6 @@ // See LICENSE in the project root for license information. using System.Text.Json; -using Duende.IdentityModel; using Duende.IdentityModel.Client; using Duende.IdentityServer; using Duende.IdentityServer.Configuration; @@ -53,7 +52,7 @@ public async Task when_lower_case_issuer_option_disabled_issuer_uri_should_be_pr [Fact] [Trait("Category", Category)] - public async Task Algorithms_supported_should_match_signing_key() + public async Task IdToken_signing_algorithms_supported_should_match_signing_key() { var key = CryptoHelper.CreateECDsaSecurityKey(JsonWebKeyECTypes.P256); var expectedAlgorithm = SecurityAlgorithms.EcdsaSha256; @@ -65,20 +64,20 @@ public async Task Algorithms_supported_should_match_signing_key() services.AddIdentityServerBuilder() .AddSigningCredential(key, expectedAlgorithm); }; - pipeline.Initialize("/ROOT"); + pipeline.Initialize(); - var result = await pipeline.BackChannelClient.GetAsync("https://server/root/.well-known/openid-configuration"); + var result = await pipeline.BackChannelClient.GetDiscoveryDocumentAsync("https://server/.well-known/openid-configuration"); - var json = await result.Content.ReadAsStringAsync(); - var data = JsonSerializer.Deserialize>(json); - var algorithmsSupported = data["id_token_signing_alg_values_supported"].EnumerateArray() - .Select(x => x.GetString()).ToList(); + var algorithmsSupported = result.TryGetStringArray("id_token_signing_alg_values_supported"); - algorithmsSupported.Count.ShouldBe(2); + algorithmsSupported.Count().ShouldBe(2); algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha256); algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha256); } + + + [Fact] [Trait("Category", Category)] public async Task Jwks_entries_should_countain_crv() @@ -333,24 +332,4 @@ public async Task par_is_included_in_mtls_aliases() var result = await pipeline.BackChannelClient.GetDiscoveryDocumentAsync("https://server/.well-known/openid-configuration"); result.MtlsEndpointAliases.PushedAuthorizationRequestEndpoint.ShouldNotBeNull(); } - - [Fact] - [Trait("Category", Category)] - public async Task dpop_signing_algorithms_supported_respects_configuration() - { - var pipeline = new IdentityServerPipeline(); - pipeline.Initialize(); - - var supportedAlgorithms = new List - { - SecurityAlgorithms.RsaSha256, - SecurityAlgorithms.EcdsaSha256 - }; - pipeline.Options.DPoP.SupportedDPoPSigningAlgorithms = supportedAlgorithms; - - var result = await pipeline.BackChannelClient.GetDiscoveryDocumentAsync("https://server/.well-known/openid-configuration"); - - var supportedAlgorithmsFromResponse = result.TryGetStringArray(OidcConstants.Discovery.DPoPSigningAlgorithmsSupported); - supportedAlgorithmsFromResponse.ShouldBe(supportedAlgorithms); - } } diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests_dpop_signing_algs_supported.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests_dpop_signing_algs_supported.cs new file mode 100644 index 000000000..7cc519c44 --- /dev/null +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests_dpop_signing_algs_supported.cs @@ -0,0 +1,87 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using System.Text.Json; +using Duende.IdentityModel; +using Duende.IdentityModel.Client; +using IntegrationTests.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace IntegrationTests.Endpoints.Discovery; + +public class DiscoveryEndpointTests_dpop_signing_alg_values_supported +{ + private const string Category = "Discovery endpoint - dpop_signing_alg_values_supported"; + + [Fact] + [Trait("Category", Category)] + public async Task dpop_signing_alg_values_supported_should_match_configuration() + { + var pipeline = new IdentityServerPipeline(); + pipeline.Initialize(); + + var supportedAlgorithms = new List + { + SecurityAlgorithms.RsaSha256, + SecurityAlgorithms.EcdsaSha256 + }; + pipeline.Options.DPoP.SupportedDPoPSigningAlgorithms = supportedAlgorithms; + + var result = + await pipeline.BackChannelClient.GetDiscoveryDocumentAsync( + "https://server/.well-known/openid-configuration"); + + var supportedAlgorithmsFromResponse = + result.TryGetStringArray(OidcConstants.Discovery.DPoPSigningAlgorithmsSupported); + supportedAlgorithmsFromResponse.ShouldBe(supportedAlgorithms); + } + + [Fact] + public async Task dpop_signing_alg_values_supported_should_default_to_rs_ps_es() + { + var pipeline = new IdentityServerPipeline(); + pipeline.Initialize(); + + var result = + await pipeline.BackChannelClient.GetDiscoveryDocumentAsync( + "https://server/.well-known/openid-configuration"); + var algorithmsSupported = result.TryGetStringArray("dpop_signing_alg_values_supported"); + + algorithmsSupported.Count().ShouldBe(9); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha512); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha512); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha512); + } + + [Theory] + [MemberData(nameof(NullOrEmptySupportedAlgorithms))] + public async Task dpop_signing_alg_values_supported_should_not_be_present_if_option_is_null_or_empty(ICollection algorithms) + { + var pipeline = new IdentityServerPipeline(); + pipeline.OnPostConfigureServices += svcs => + svcs.AddIdentityServerBuilder().AddJwtBearerClientAuthentication(); + pipeline.Initialize(); + pipeline.Options.DPoP.SupportedDPoPSigningAlgorithms = algorithms; + + var result = await pipeline.BackChannelClient + .GetAsync("https://server/.well-known/openid-configuration"); + var json = await result.Content.ReadAsStringAsync(); + var data = JsonSerializer.Deserialize>(json); + + data.ShouldNotContainKey(OidcConstants.Discovery.DPoPSigningAlgorithmsSupported); + } + + public static IEnumerable NullOrEmptySupportedAlgorithms() => + new List + { + new object[] { Enumerable.Empty() }, + new object[] { null } + }; +} diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests_token_endpoint_auth_signing_algs_supported.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests_token_endpoint_auth_signing_algs_supported.cs new file mode 100644 index 000000000..d8f32491a --- /dev/null +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpointTests_token_endpoint_auth_signing_algs_supported.cs @@ -0,0 +1,120 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using System.Text.Json; +using Duende.IdentityModel; +using Duende.IdentityModel.Client; +using IntegrationTests.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace IntegrationTests.Endpoints.Discovery; + +public class DiscoveryEndpointTests_token_endpoint_auth_signing_alg_values_supported +{ + private const string Category = "Discovery endpoint - token_endpoint_auth_signing_alg_values_supported"; + + [Fact] + [Trait("Category", Category)] + public async Task token_endpoint_auth_signing_alg_values_supported_should_match_configuration() + { + var pipeline = new IdentityServerPipeline(); + pipeline.OnPostConfigureServices += svcs => + svcs.AddIdentityServerBuilder().AddJwtBearerClientAuthentication(); + pipeline.Initialize(); + pipeline.Options.SupportedClientAssertionSigningAlgorithms = + [ + SecurityAlgorithms.RsaSsaPssSha256, + SecurityAlgorithms.EcdsaSha256 + ]; + + var disco = await pipeline.BackChannelClient + .GetDiscoveryDocumentAsync("https://server/.well-known/openid-configuration"); + disco.IsError.ShouldBeFalse(); + + var algorithmsSupported = disco.TokenEndpointAuthenticationSigningAlgorithmsSupported; + + algorithmsSupported.Count().ShouldBe(2); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha256); + } + + [Fact] + [Trait("Category", Category)] + public async Task token_endpoint_auth_signing_alg_values_supported_should_default_to_rs_ps_es_hmac() + { + var pipeline = new IdentityServerPipeline(); + pipeline.OnPostConfigureServices += svcs => + svcs.AddIdentityServerBuilder().AddJwtBearerClientAuthentication(); + pipeline.Initialize(); + + var result = + await pipeline.BackChannelClient.GetDiscoveryDocumentAsync( + "https://server/.well-known/openid-configuration"); + + result.IsError.ShouldBeFalse(); + var algorithmsSupported = result.TokenEndpointAuthenticationSigningAlgorithmsSupported; + + algorithmsSupported.Count().ShouldBe(12); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha512); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha512); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha512); + algorithmsSupported.ShouldContain(SecurityAlgorithms.HmacSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.HmacSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.HmacSha512); + } + + [Fact] + [Trait("Category", Category)] + public async Task token_endpoint_auth_signing_alg_values_supported_should_not_be_present_if_private_key_jwt_is_not_configured() + { + var pipeline = new IdentityServerPipeline(); + pipeline.Initialize(); + pipeline.Options.SupportedClientAssertionSigningAlgorithms = [SecurityAlgorithms.RsaSha256]; + + var disco = await pipeline.BackChannelClient + .GetDiscoveryDocumentAsync("https://server/.well-known/openid-configuration"); + + // Verify assumptions + disco.IsError.ShouldBeFalse(); + disco.TokenEndpointAuthenticationMethodsSupported.ShouldNotContain("private_key_jwt"); + // we don't even support client_secret_jwt, but per spec, if you DO, you must include the algs supported + disco.TokenEndpointAuthenticationMethodsSupported.ShouldNotContain("client_secret_jwt"); + + // Assert that we got no signing algs. + disco.TokenEndpointAuthenticationSigningAlgorithmsSupported.ShouldBeEmpty(); + } + + [Theory] + [MemberData(nameof(NullOrEmptySupportedAlgorithms))] + [Trait("Category", Category)] + public async Task token_endpoint_auth_signing_alg_values_supported_should_not_be_present_if_option_is_null_or_empty( + ICollection algorithms) + { + var pipeline = new IdentityServerPipeline(); + pipeline.OnPostConfigureServices += svcs => + svcs.AddIdentityServerBuilder().AddJwtBearerClientAuthentication(); + pipeline.Initialize(); + pipeline.Options.SupportedClientAssertionSigningAlgorithms = algorithms; + + var result = await pipeline.BackChannelClient + .GetAsync("https://server/.well-known/openid-configuration"); + var json = await result.Content.ReadAsStringAsync(); + var data = JsonSerializer.Deserialize>(json); + + data.ShouldNotContainKey(OidcConstants.Discovery.TokenEndpointAuthSigningAlgorithmsSupported); + } + + public static IEnumerable NullOrEmptySupportedAlgorithms() => + new List + { + new object[] { Enumerable.Empty() }, + new object[] { null } + }; +} diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpoint_request_object_auth_signing_algs_supported.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpoint_request_object_auth_signing_algs_supported.cs new file mode 100644 index 000000000..3491b6ca4 --- /dev/null +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Discovery/DiscoveryEndpoint_request_object_auth_signing_algs_supported.cs @@ -0,0 +1,89 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using System.Text.Json; +using Duende.IdentityModel.Client; +using IntegrationTests.Common; +using Microsoft.IdentityModel.Tokens; + +namespace IntegrationTests.Endpoints.Discovery; + +public class DiscoveryEndpoint_request_object_auth_signing_algs_supported_Tests +{ + private const string Category = "Discovery endpoint - request_object_signing_algs_supported"; + + [Fact] + [Trait("Category", Category)] + public async Task request_object_signing_alg_values_supported_should_match_configuration() + { + var pipeline = new IdentityServerPipeline(); + + pipeline.Initialize(); + pipeline.Options.SupportedRequestObjectSigningAlgorithms = + [ + SecurityAlgorithms.RsaSsaPssSha256, + SecurityAlgorithms.EcdsaSha256 + ]; + + var result = + await pipeline.BackChannelClient.GetDiscoveryDocumentAsync( + "https://server/.well-known/openid-configuration"); + var algorithmsSupported = result.TryGetStringArray("request_object_signing_alg_values_supported"); + + algorithmsSupported.Count().ShouldBe(2); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha256); + } + + [Theory] + [MemberData(nameof(NullOrEmptySupportedAlgorithms))] + [Trait("Category", Category)] + public async Task request_object_signing_alg_values_supported_should_not_be_present_if_option_is_null_or_empty( + ICollection algorithms) + { + var pipeline = new IdentityServerPipeline(); + pipeline.Initialize(); + pipeline.Options.SupportedRequestObjectSigningAlgorithms = algorithms; + + var result = await pipeline.BackChannelClient + .GetAsync("https://server/.well-known/openid-configuration"); + var json = await result.Content.ReadAsStringAsync(); + var data = JsonSerializer.Deserialize>(json); + + data.ShouldNotContainKey("request_object_signing_alg_values_supported"); + } + + [Fact] + [Trait("Category", Category)] + public async Task request_object_signing_alg_values_supported_should_default_to_rs_ps_es_hmac() + { + var pipeline = new IdentityServerPipeline(); + pipeline.Initialize(); + + var result = + await pipeline.BackChannelClient.GetDiscoveryDocumentAsync( + "https://server/.well-known/openid-configuration"); + var algorithmsSupported = result.TryGetStringArray("request_object_signing_alg_values_supported"); + + algorithmsSupported.Count().ShouldBe(12); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSha512); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha512); + algorithmsSupported.ShouldContain(SecurityAlgorithms.RsaSsaPssSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.EcdsaSha512); + algorithmsSupported.ShouldContain(SecurityAlgorithms.HmacSha256); + algorithmsSupported.ShouldContain(SecurityAlgorithms.HmacSha384); + algorithmsSupported.ShouldContain(SecurityAlgorithms.HmacSha512); + } + + public static IEnumerable NullOrEmptySupportedAlgorithms() => + new List + { + new object[] { Enumerable.Empty() }, + new object[] { null } + }; +} diff --git a/identity-server/test/IdentityServer.UnitTests/Validation/AccessTokenValidation.cs b/identity-server/test/IdentityServer.UnitTests/Validation/AccessTokenValidation.cs index 911edf8f9..24c8d7ba1 100644 --- a/identity-server/test/IdentityServer.UnitTests/Validation/AccessTokenValidation.cs +++ b/identity-server/test/IdentityServer.UnitTests/Validation/AccessTokenValidation.cs @@ -269,19 +269,19 @@ public async Task Jwt_when_token_time_outside_of_configured_clock_skew_token_is_ [Fact] [Trait("Category", Category)] - public async Task Jwt_created_with_signing_algorithm_not_allowed_by_identity_server_settings_is_considered_invalid() + public async Task Unrelated_supported_signing_algorithm_options_are_not_enforced() { var signer = Factory.CreateDefaultTokenCreator(); var token = TokenFactory.CreateAccessToken(new Client { ClientId = "roclient" }, "valid", 600, "read", "write"); var jwt = await signer.CreateTokenAsync(token); var options = TestIdentityServerOptions.Create(); - options.AllowedJwtAlgorithms = ["Test"]; + options.SupportedRequestObjectSigningAlgorithms = ["Test"]; + options.SupportedClientAssertionSigningAlgorithms = ["Test"]; var validator = Factory.CreateTokenValidator(options: options); var result = await validator.ValidateAccessTokenAsync(jwt); - result.IsError.ShouldBeTrue(); - result.Error.ShouldBe(OidcConstants.ProtectedResourceErrors.InvalidToken); + result.IsError.ShouldBeFalse(); } [Fact] diff --git a/identity-server/test/IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs b/identity-server/test/IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs index 3de1c1b1e..a1f0091d6 100644 --- a/identity-server/test/IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs +++ b/identity-server/test/IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs @@ -473,7 +473,7 @@ public async Task Signing_Algorithm_Not_Allowed_By_Configuration() Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer }; - _options.AllowedJwtAlgorithms = ["Test"]; + _options.SupportedClientAssertionSigningAlgorithms = ["Test"]; var result = await _validator.ValidateAsync(client.ClientSecrets, secret);