Skip to content

Commit f6b3471

Browse files
committed
Updated PAR validation to handle case where client authenticated with a cert and sent dpop proof token
1 parent aa35bc1 commit f6b3471

File tree

5 files changed

+63
-9
lines changed

5 files changed

+63
-9
lines changed

identity-server/src/IdentityServer/Endpoints/PushedAuthorizationEndpoint.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ ILogger<PushedAuthorizationEndpoint> logger
8686
}
8787

8888
validationContext.DPoPProofToken = dpopHeader.First();
89+
//Note: if the client authenticated with mTLS, we need to know to properly validate the htu of the DPoP proof token
90+
validationContext.ClientCertificate = await context.Connection.GetClientCertificateAsync();
8991
}
9092

9193
// Perform validations specific to PAR, as well as validation of the pushed parameters

identity-server/src/IdentityServer/Validation/Contexts/PushedAuthorizationRequestValidationContext.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44

55
using System.Collections.Specialized;
6+
using System.Security.Cryptography.X509Certificates;
67
using Duende.IdentityServer.Models;
78

89
namespace Duende.IdentityServer.Validation;
@@ -33,6 +34,11 @@ public PushedAuthorizationRequestValidationContext(NameValueCollection requestPa
3334
/// </summary>
3435
public Client Client { get; set; }
3536

37+
/// <summary>
38+
/// The client certificate used on the mTLS connection.
39+
/// </summary>
40+
public X509Certificate2 ClientCertificate { get; set; }
41+
3642
/// <summary>
3743
/// The DPoP proof token sent to the endpoint, if any
3844
/// </summary>

identity-server/src/IdentityServer/Validation/Default/PushedAuthorizationRequestValidator.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ namespace Duende.IdentityServer.Validation;
1818
/// Default validator for pushed authorization requests. This validator performs
1919
/// checks that are specific to pushed authorization and also invokes the <see
2020
/// cref="IAuthorizeRequestValidator"/> to validate the pushed parameters as if
21-
/// they had been sent to the authorize endpoint directly.
21+
/// they had been sent to the authorize endpoint directly.
2222
/// </summary>
2323
/// <remarks>
2424
/// Initializes a new instance of the <see
25-
/// cref="PushedAuthorizationRequestValidator"/> class.
25+
/// cref="PushedAuthorizationRequestValidator"/> class.
2626
/// </remarks>
2727
/// <param name="authorizeRequestValidator">The authorize request validator,
2828
/// used to validate the pushed authorization parameters as if they were
@@ -33,13 +33,15 @@ namespace Duende.IdentityServer.Validation;
3333
/// <param name="serverUrls">The server urls service</param>
3434
/// <param name="licenseUsage">The feature manager</param>
3535
/// <param name="options">The IdentityServer Options</param>
36+
/// <param name="mtlsEndpointGenerator">The mTLS endpoint generator</param>
3637
/// <param name="logger">The logger</param>
3738
internal class PushedAuthorizationRequestValidator(
3839
IAuthorizeRequestValidator authorizeRequestValidator,
3940
IDPoPProofValidator dpopProofValidator,
4041
IServerUrls serverUrls,
4142
LicenseUsageTracker licenseUsage,
4243
IdentityServerOptions options,
44+
IMtlsEndpointGenerator mtlsEndpointGenerator,
4345
ILogger<PushedAuthorizationRequestValidator> logger) : IPushedAuthorizationRequestValidator
4446
{
4547
public async Task<PushedAuthorizationValidationResult> ValidateAsync(PushedAuthorizationRequestValidationContext context)
@@ -57,17 +59,17 @@ public async Task<PushedAuthorizationValidationResult> ValidateAsync(PushedAutho
5759

5860
// -- DPoP Header Validation --
5961
// The client can send the public key of its DPoP proof key to us. We
60-
// then bind its authorization code to the proof key and check for a
62+
// then bind its authorization code to the proof key and check for a
6163
// proof token signed with the key at the token endpoint.
62-
//
63-
// There are two ways for the client to send its DPoP proof key public
64+
//
65+
// There are two ways for the client to send its DPoP proof key public
6466
// key material to us:
6567
// 1. pass the dpop_jkt parameter with a JWK thumbprint (RFC 7638)
66-
// 2. send a DPoP proof (which contains the public key as a JWK) in the
68+
// 2. send a DPoP proof (which contains the public key as a JWK) in the
6769
// DPoP http header
6870
//
69-
// If a proof is passed, then we validate it, compute the thumbprint of
70-
// the key within, and treat that as if it were passed as the dpop_jkt
71+
// If a proof is passed, then we validate it, compute the thumbprint of
72+
// the key within, and treat that as if it were passed as the dpop_jkt
7173
// parameter.
7274
//
7375
// If a proof and a dpop_jkt are both passed, its an error if they don't
@@ -84,7 +86,7 @@ public async Task<PushedAuthorizationValidationResult> ValidateAsync(PushedAutho
8486
}
8587

8688
// validate proof token
87-
var parUrl = serverUrls.BaseUrl.EnsureTrailingSlash() + ProtocolRoutePaths.PushedAuthorization;
89+
var parUrl = context.ClientCertificate == null ? serverUrls.BaseUrl.EnsureTrailingSlash() + ProtocolRoutePaths.PushedAuthorization : mtlsEndpointGenerator.GetMtlsEndpointPath(ProtocolRoutePaths.PushedAuthorization);
8890
var dpopContext = new DPoPProofValidatonContext
8991
{
9092
ProofToken = context.DPoPProofToken,

identity-server/test/IdentityServer.IntegrationTests/Common/IdentityServerPipeline.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class IdentityServerPipeline
4949
public const string EndSessionCallbackEndpoint = BaseUrl + "/connect/endsession/callback";
5050
public const string CheckSessionEndpoint = BaseUrl + "/connect/checksession";
5151
public const string ParEndpoint = BaseUrl + "/connect/par";
52+
public const string ParMtlsEndpoint = BaseUrl + "/connect/mtls/par";
5253

5354

5455
public const string FederatedSignOutPath = "/signout-oidc";

identity-server/test/IdentityServer.IntegrationTests/Endpoints/Token/DPoPPushedAuthorizationEndpointTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
using Duende.IdentityModel;
66
using Duende.IdentityModel.Client;
7+
using Duende.IdentityServer;
8+
using Duende.IdentityServer.Models;
79
using IntegrationTests.Common;
810
using IntegrationTests.Endpoints.Token;
11+
using PushedAuthorizationRequest = Duende.IdentityModel.Client.PushedAuthorizationRequest;
912

1013
namespace IntegrationTests.Endpoints.PushedAuthorization;
1114

@@ -107,4 +110,44 @@ public async Task mismatch_between_header_and_thumbprint_should_fail()
107110
response.IsError.ShouldBeTrue();
108111
response.Error.ShouldBe(OidcConstants.AuthorizeErrors.InvalidRequest);
109112
}
113+
114+
[Fact]
115+
public async Task push_authorization_with_mtls_client_auth_and_dpop_should_succeed()
116+
{
117+
var clientId = "mtls_dpop_client";
118+
var clientCert = TestCert.Load();
119+
120+
// Add a client that requires mTLS and supports DPoP
121+
var client = new Client
122+
{
123+
ClientId = clientId,
124+
ClientSecrets =
125+
{
126+
new Secret
127+
{
128+
Type = IdentityServerConstants.SecretTypes.X509CertificateThumbprint,
129+
Value = clientCert.Thumbprint
130+
}
131+
},
132+
AllowedGrantTypes = GrantTypes.Code,
133+
RedirectUris = { "https://client1/callback" },
134+
AllowedScopes = { "scope1" },
135+
RequireDPoP = true
136+
};
137+
138+
Pipeline.Clients.Add(client);
139+
Pipeline.Initialize();
140+
141+
// Set the client certificate in the pipeline
142+
Pipeline.SetClientCertificate(clientCert);
143+
144+
var tokenClient = Pipeline.GetMtlsClient();
145+
var proofToken = CreateDPoPProofToken(htu: IdentityServerPipeline.ParMtlsEndpoint);
146+
tokenClient.DefaultRequestHeaders.Add("DPoP", proofToken);
147+
var request = CreatePushedAuthorizationRequest(proofToken);
148+
149+
var response = await tokenClient.PushAuthorizationAsync(request);
150+
151+
response.IsError.ShouldBeFalse();
152+
}
110153
}

0 commit comments

Comments
 (0)