Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#nullable enable

using Microsoft.IdentityModel.Tokens;

namespace Duende.IdentityServer.Configuration;

/// <summary>
Expand All @@ -20,4 +22,23 @@ public class DPoPOptions
/// Clock skew used in validating DPoP proof token expiration using a server-generated nonce value. Defaults to ten seconds.
/// </summary>
public TimeSpan ServerClockSkew { get; set; } = TimeSpan.FromSeconds(10);

/// <summary>
/// The allowed signing algorithms used in validating DPoP proof tokens. Defaults to:
/// RSA256, RSA384, RSA512, PS256, PS384, PS512, ES256, ES384, ES512.
/// </summary>
public ICollection<string> SupportedDPoPSigningAlgorithms { get; set; } =
[
SecurityAlgorithms.RsaSha256,
SecurityAlgorithms.RsaSha384,
SecurityAlgorithms.RsaSha512,

SecurityAlgorithms.RsaSsaPssSha256,
SecurityAlgorithms.RsaSsaPssSha384,
SecurityAlgorithms.RsaSsaPssSha512,

SecurityAlgorithms.EcdsaSha256,
SecurityAlgorithms.EcdsaSha384,
SecurityAlgorithms.EcdsaSha512
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,16 +205,28 @@ public class IdentityServerOptions
public PushedAuthorizationOptions PushedAuthorization { get; set; } = new PushedAuthorizationOptions();

/// <summary>
/// The allowed clock skew for JWT lifetime validation. 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 <see cref="TokenValidator"/>,
/// such as in a token exchange implementation. Defaults to ten seconds.
/// 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
/// <see cref="TokenValidator"/>, such as in a token exchange implementation.
/// Defaults to ten seconds.
/// </summary>
public TimeSpan JwtValidationClockSkew { get; set; } = TimeSpan.FromSeconds(10);

/// <summary>
/// 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 <see cref="TokenValidator"/>,
/// such as in a token exchange implementation. Defaults to an empty collection which
/// allows all algorithms.
/// </summary>
public ICollection<string> AllowedJwtAlgorithms { get; set; } = [];

/// <summary>
/// Gets or sets the options for enabling and configuring preview features in the server.
/// Preview features provide access to experimental or in-progress functionality that may undergo
Expand Down
15 changes: 0 additions & 15 deletions identity-server/src/IdentityServer/IdentityServerConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,21 +117,6 @@ public static class ProfileIsActiveCallers
SecurityAlgorithms.EcdsaSha512
};

public readonly static IEnumerable<string> SupportedDPoPSigningAlgorithms = new[]
{
SecurityAlgorithms.RsaSha256,
SecurityAlgorithms.RsaSha384,
SecurityAlgorithms.RsaSha512,

SecurityAlgorithms.RsaSsaPssSha256,
SecurityAlgorithms.RsaSsaPssSha384,
SecurityAlgorithms.RsaSsaPssSha512,

SecurityAlgorithms.EcdsaSha256,
SecurityAlgorithms.EcdsaSha384,
SecurityAlgorithms.EcdsaSha512
};

public enum RsaSigningAlgorithm
{
RS256,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ where scope.ShowInDiscoveryDocument

if (Options.Endpoints.EnableTokenEndpoint)
{
entries.Add(OidcConstants.Discovery.DPoPSigningAlgorithmsSupported, SupportedDPoPSigningAlgorithms);
entries.Add(OidcConstants.Discovery.DPoPSigningAlgorithmsSupported, Options.DPoP.SupportedDPoPSigningAlgorithms);
}

// custom entries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ protected virtual Task ValidateHeaderAsync(DPoPProofValidatonContext context, DP
return Task.CompletedTask;
}

if (!token.TryGetHeaderValue<string>(JwtClaimTypes.Algorithm, out var alg) || !IdentityServerConstants.SupportedDPoPSigningAlgorithms.Contains(alg))
if (!token.TryGetHeaderValue<string>(JwtClaimTypes.Algorithm, out var alg) || !Options.DPoP.SupportedDPoPSigningAlgorithms.Contains(alg))
{
result.IsError = true;
result.ErrorDescription = "Invalid 'alg' value.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ protected virtual async Task<JsonWebToken> ValidateJwtAsync(JwtRequestValidation
RequireSignedTokens = true,
RequireExpirationTime = true,

ClockSkew = Options.JwtValidationClockSkew
ClockSkew = Options.JwtValidationClockSkew,
ValidAlgorithms = Options.AllowedJwtAlgorithms
};

var strictJarValidation = context.StrictJarValidation.HasValue ? context.StrictJarValidation.Value : Options.StrictJarValidation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ public async Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secr
RequireSignedTokens = true,
RequireExpirationTime = true,

ClockSkew = _options.JwtValidationClockSkew
ClockSkew = _options.JwtValidationClockSkew,
ValidAlgorithms = _options.AllowedJwtAlgorithms
};

var issuer = await _issuerNameService.GetCurrentAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ private async Task<TokenValidationResult> ValidateJwtAsync(string jwtString,
ValidIssuer = await _issuerNameService.GetCurrentAsync(),
IssuerSigningKeys = validationKeys.Select(k => k.Key),
ValidateLifetime = validateLifetime,
ClockSkew = _options.JwtValidationClockSkew
ClockSkew = _options.JwtValidationClockSkew,
ValidAlgorithms = _options.AllowedJwtAlgorithms
};

if (audience.IsPresent())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,43 @@ public async Task authorize_should_reject_jwt_request_if_client_id_does_not_matc
_mockPipeline.LoginRequest.ShouldBeNull();
}

[Fact]
[Trait("Category", Category)]
public async Task authorize_should_reject_jwt_request_if_signed_by_algorithm_not_allowed_by_configuration()
{
_mockPipeline.Options.AllowedJwtAlgorithms = ["ES256"];
var requestJwt = CreateRequestJwt(
issuer: _client.ClientId,
audience: IdentityServerPipeline.BaseUrl,
credential: new SigningCredentials(_rsaKey, "RS256"),
claims: new[] {
new Claim("client_id", _client.ClientId),
new Claim("response_type", "id_token"),
new Claim("scope", "openid profile"),
new Claim("state", "123state"),
new Claim("nonce", "123nonce"),
new Claim("redirect_uri", "https://client/callback"),
new Claim("acr_values", "acr_1 acr_2 tenant:tenant_value idp:idp_value"),
new Claim("login_hint", "login_hint_value"),
new Claim("display", "popup"),
new Claim("ui_locales", "ui_locale_value"),
new Claim("foo", "123foo"),
});

var url = _mockPipeline.CreateAuthorizeUrl(
clientId: _client.ClientId,
responseType: "id_token",
extra: new
{
request = requestJwt
});
_ = await _mockPipeline.BrowserClient.GetAsync(url);

_mockPipeline.ErrorMessage.Error.ShouldBe("invalid_request_object");
_mockPipeline.ErrorMessage.ErrorDescription.ShouldBe("Invalid JWT request");
_mockPipeline.LoginRequest.ShouldBeNull();
}

[Fact]
[Trait("Category", Category)]
public async Task authorize_should_ignore_request_uri_when_feature_is_disabled()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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;
Expand Down Expand Up @@ -332,4 +333,24 @@ 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<string>
{
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,23 @@ public async Task Jwt_when_token_time_outside_of_configured_clock_skew_token_is_
result.Error.ShouldBe(OidcConstants.ProtectedResourceErrors.InvalidToken);
}

[Fact]
[Trait("Category", Category)]
public async Task Jwt_created_with_signing_algorithm_not_allowed_by_identity_server_settings_is_considered_invalid()
{
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"];
var validator = Factory.CreateTokenValidator(options: options);
var result = await validator.ValidateAccessTokenAsync(jwt);

result.IsError.ShouldBeTrue();
result.Error.ShouldBe(OidcConstants.ProtectedResourceErrors.InvalidToken);
}

[Fact]
[Trait("Category", Category)]
public async Task Valid_AccessToken_but_Client_not_active()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ public async Task invalid_typ_should_fail_validation()
[Trait("Category", Category)]
public async Task invalid_alg_should_fail_validation()
{
_options.DPoP.SupportedDPoPSigningAlgorithms = [SecurityAlgorithms.EcdsaSha512];
var key = new SymmetricSecurityKey(Duende.IdentityModel.CryptoRandom.CreateRandomKey(32));
_publicJWK = JsonSerializer.Serialize(key);
CreateHeaderValuesFromPublicKey();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,4 +458,25 @@ public async Task Invalid_Not_Yet_Valid_Token()

result.Success.ShouldBeFalse();
}

[Fact]
public async Task Signing_Algorithm_Not_Allowed_By_Configuration()
{
var clientId = "certificate_base64_valid";
var client = await _clients.FindEnabledClientByIdAsync(clientId);

var token = CreateToken(clientId);
var secret = new ParsedSecret
{
Id = clientId,
Credential = new JwtSecurityTokenHandler().WriteToken(token),
Type = IdentityServerConstants.ParsedSecretTypes.JwtBearer
};

_options.AllowedJwtAlgorithms = ["Test"];

var result = await _validator.ValidateAsync(client.ClientSecrets, secret);

result.Success.ShouldBeFalse();
}
}
Loading