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 @@ -183,6 +183,7 @@ public static IIdentityServerBuilder AddHttpWriter<TResult, TWriter>(this IIdent
public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder builder)
{
builder.Services.AddTransient<IServerUrls, DefaultServerUrls>();
builder.Services.AddTransient<IMtlsEndpointGenerator, DefaultMtlsEndpointGenerator>();
builder.Services.AddTransient<IIssuerNameService, DefaultIssuerNameService>();
builder.Services.AddTransient<ISecretsListParser, SecretParser>();
builder.Services.AddTransient<ISecretsListValidator, SecretValidator>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ ILogger<PushedAuthorizationEndpoint> logger
}

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

// Perform validations specific to PAR, as well as validation of the pushed parameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ public virtual async Task<Dictionary<string, object>> CreateDiscoveryDocumentAsy
entries.Add(OidcConstants.Discovery.MtlsEndpointAliases, mtlsEndpoints);
}

//Note: This logic is currently duplicated in the DefaultMtlsEndpointGenerator as adding a new
//dependency here would be a breaking change in a non-major release.
string ConstructMtlsEndpoint(string endpoint)
{
// path based
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Extensions;
using Microsoft.Extensions.Options;

namespace Duende.IdentityServer.Services.Default;

internal class DefaultMtlsEndpointGenerator(IServerUrls serverUrls, IOptions<IdentityServerOptions> options)
: IMtlsEndpointGenerator
{
//Note: This logic is currently duplicated in the DiscoveryResponseGenerator as adding a new
//dependency there would be a breaking change in a non-major release.
public string GetMtlsEndpointPath(string endpoint)
{
var baseUrl = serverUrls.BaseUrl.EnsureTrailingSlash();

// path based
if (options.Value.MutualTls.DomainName.IsMissing())
{
return baseUrl + endpoint.Replace(IdentityServerConstants.ProtocolRoutePaths.ConnectPathPrefix, IdentityServerConstants.ProtocolRoutePaths.MtlsPathPrefix);
}

// domain based
if (options.Value.MutualTls.DomainName.Contains('.'))
{
return $"https://{options.Value.MutualTls.DomainName}/{endpoint}";
}
// sub-domain based
else
{
var parts = baseUrl.Split("://");
return $"https://{options.Value.MutualTls.DomainName}.{parts[1]}{endpoint}";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

namespace Duende.IdentityServer.Services;

internal interface IMtlsEndpointGenerator
{
string GetMtlsEndpointPath(string endpoint);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


using System.Collections.Specialized;
using System.Security.Cryptography.X509Certificates;
using Duende.IdentityServer.Models;

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

/// <summary>
/// The client certificate used on the mTLS connection.
/// </summary>
public X509Certificate2 ClientCertificate { get; set; }

/// <summary>
/// The DPoP proof token sent to the endpoint, if any
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ namespace Duende.IdentityServer.Validation;
/// Default validator for pushed authorization requests. This validator performs
/// checks that are specific to pushed authorization and also invokes the <see
/// cref="IAuthorizeRequestValidator"/> to validate the pushed parameters as if
/// they had been sent to the authorize endpoint directly.
/// they had been sent to the authorize endpoint directly.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see
/// cref="PushedAuthorizationRequestValidator"/> class.
/// cref="PushedAuthorizationRequestValidator"/> class.
/// </remarks>
/// <param name="authorizeRequestValidator">The authorize request validator,
/// used to validate the pushed authorization parameters as if they were
Expand All @@ -33,13 +33,15 @@ namespace Duende.IdentityServer.Validation;
/// <param name="serverUrls">The server urls service</param>
/// <param name="licenseUsage">The feature manager</param>
/// <param name="options">The IdentityServer Options</param>
/// <param name="mtlsEndpointGenerator">The mTLS endpoint generator</param>
/// <param name="logger">The logger</param>
internal class PushedAuthorizationRequestValidator(
IAuthorizeRequestValidator authorizeRequestValidator,
IDPoPProofValidator dpopProofValidator,
IServerUrls serverUrls,
LicenseUsageTracker licenseUsage,
IdentityServerOptions options,
IMtlsEndpointGenerator mtlsEndpointGenerator,
ILogger<PushedAuthorizationRequestValidator> logger) : IPushedAuthorizationRequestValidator
{
public async Task<PushedAuthorizationValidationResult> ValidateAsync(PushedAuthorizationRequestValidationContext context)
Expand All @@ -57,17 +59,17 @@ public async Task<PushedAuthorizationValidationResult> ValidateAsync(PushedAutho

// -- DPoP Header Validation --
// The client can send the public key of its DPoP proof key to us. We
// then bind its authorization code to the proof key and check for a
// then bind its authorization code to the proof key and check for a
// proof token signed with the key at the token endpoint.
//
// There are two ways for the client to send its DPoP proof key public
//
// There are two ways for the client to send its DPoP proof key public
// key material to us:
// 1. pass the dpop_jkt parameter with a JWK thumbprint (RFC 7638)
// 2. send a DPoP proof (which contains the public key as a JWK) in the
// 2. send a DPoP proof (which contains the public key as a JWK) in the
// DPoP http header
//
// If a proof is passed, then we validate it, compute the thumbprint of
// the key within, and treat that as if it were passed as the dpop_jkt
// If a proof is passed, then we validate it, compute the thumbprint of
// the key within, and treat that as if it were passed as the dpop_jkt
// parameter.
//
// If a proof and a dpop_jkt are both passed, its an error if they don't
Expand All @@ -84,7 +86,7 @@ public async Task<PushedAuthorizationValidationResult> ValidateAsync(PushedAutho
}

// validate proof token
var parUrl = serverUrls.BaseUrl.EnsureTrailingSlash() + ProtocolRoutePaths.PushedAuthorization;
var parUrl = context.ClientCertificate == null ? serverUrls.BaseUrl.EnsureTrailingSlash() + ProtocolRoutePaths.PushedAuthorization : mtlsEndpointGenerator.GetMtlsEndpointPath(ProtocolRoutePaths.PushedAuthorization);
var dpopContext = new DPoPProofValidatonContext
{
ProofToken = context.DPoPProofToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal class TokenRequestValidator : ITokenRequestValidator
private readonly LicenseUsageTracker _licenseUsage;
private readonly ClientLoadedTracker _clientLoadedTracker;
private readonly ResourceLoadedTracker _resourceLoadedTracker;
private readonly IMtlsEndpointGenerator _mtlsEndpointGenerator;
private readonly ILogger _logger;

private ValidatedTokenRequest _validatedRequest;
Expand All @@ -64,6 +65,7 @@ public TokenRequestValidator(
LicenseUsageTracker licenseUsage,
ClientLoadedTracker clientLoadedTracker,
ResourceLoadedTracker resourceLoadedTracker,
IMtlsEndpointGenerator mtlsEndpointGenerator,
ILogger<TokenRequestValidator> logger)
{
_logger = logger;
Expand All @@ -86,6 +88,7 @@ public TokenRequestValidator(
_events = events;
_clientLoadedTracker = clientLoadedTracker;
_resourceLoadedTracker = resourceLoadedTracker;
_mtlsEndpointGenerator = mtlsEndpointGenerator;
}

// only here for legacy unit tests
Expand Down Expand Up @@ -248,7 +251,7 @@ private async Task<TokenRequestValidationResult> ValidateProofToken(TokenRequest
return Invalid(OidcConstants.TokenErrors.InvalidDPoPProof);
}

var tokenUrl = _serverUrls.BaseUrl.EnsureTrailingSlash() + ProtocolRoutePaths.Token;
var tokenUrl = context.ClientCertificate == null ? _serverUrls.BaseUrl.EnsureTrailingSlash() + ProtocolRoutePaths.Token : _mtlsEndpointGenerator.GetMtlsEndpointPath(ProtocolRoutePaths.Token);
var dpopContext = new DPoPProofValidatonContext
{
ExpirationValidationMode = _validatedRequest.Client.DPoPValidationMode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class IdentityServerPipeline
public const string EndSessionCallbackEndpoint = BaseUrl + "/connect/endsession/callback";
public const string CheckSessionEndpoint = BaseUrl + "/connect/checksession";
public const string ParEndpoint = BaseUrl + "/connect/par";
public const string ParMtlsEndpoint = BaseUrl + "/connect/mtls/par";


public const string FederatedSignOutPath = "/signout-oidc";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

using Duende.IdentityModel;
using Duende.IdentityModel.Client;
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using IntegrationTests.Common;
using IntegrationTests.Endpoints.Token;
using PushedAuthorizationRequest = Duende.IdentityModel.Client.PushedAuthorizationRequest;

namespace IntegrationTests.Endpoints.PushedAuthorization;

Expand Down Expand Up @@ -107,4 +110,44 @@ public async Task mismatch_between_header_and_thumbprint_should_fail()
response.IsError.ShouldBeTrue();
response.Error.ShouldBe(OidcConstants.AuthorizeErrors.InvalidRequest);
}

[Fact]
public async Task push_authorization_with_mtls_client_auth_and_dpop_should_succeed()
{
var clientId = "mtls_dpop_client";
var clientCert = TestCert.Load();

// Add a client that requires mTLS and supports DPoP
var client = new Client
{
ClientId = clientId,
ClientSecrets =
{
new Secret
{
Type = IdentityServerConstants.SecretTypes.X509CertificateThumbprint,
Value = clientCert.Thumbprint
}
},
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://client1/callback" },
AllowedScopes = { "scope1" },
RequireDPoP = true
};

Pipeline.Clients.Add(client);
Pipeline.Initialize();

// Set the client certificate in the pipeline
Pipeline.SetClientCertificate(clientCert);

var tokenClient = Pipeline.GetMtlsClient();
var proofToken = CreateDPoPProofToken(htu: IdentityServerPipeline.ParMtlsEndpoint);
tokenClient.DefaultRequestHeaders.Add("DPoP", proofToken);
var request = CreatePushedAuthorizationRequest(proofToken);

var response = await tokenClient.PushAuthorizationAsync(request);

response.IsError.ShouldBeFalse();
}
}
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.Net;
using Duende.IdentityModel;
using Duende.IdentityModel.Client;
using Duende.IdentityServer;
Expand Down Expand Up @@ -392,6 +393,50 @@ public async Task server_issued_nonce_should_be_emitted(ParMode parMode)
codeResponse.DPoPNonce.ShouldBe(expectedNonce);
}

[Fact]
[Trait("Category", Category)]
public async Task token_request_when_using_mtls_for_client_authentication_should_succeed()
{
var clientId = "mtls_client";
var clientCert = TestCert.Load();
var client = new Client
{
ClientId = clientId,
ClientSecrets =
{
new Secret
{
Type = IdentityServerConstants.SecretTypes.X509CertificateThumbprint,
Value = clientCert.Thumbprint
}
},
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "scope1" },
RequireDPoP = true
};

Pipeline.Clients.Add(client);
Pipeline.Initialize();
Pipeline.SetClientCertificate(clientCert);

var tokenClient = Pipeline.GetMtlsClient();
tokenClient.DefaultRequestHeaders.Add("DPoP", CreateDPoPProofToken(htu: IdentityServerPipeline.TokenMtlsEndpoint));
var formParams = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", clientId },
{ "scope", "scope1" }
};

var form = new FormUrlEncodedContent(formParams);
var response = await tokenClient.PostAsync(IdentityServerPipeline.TokenMtlsEndpoint, form);

response.StatusCode.ShouldBe(HttpStatusCode.OK);
var json = await response.Content.ReadAsStringAsync();
json.ShouldContain("access_token");
json.ShouldContain("\"token_type\":\"DPoP\"");
}

internal class MockDPoPProofValidator : DefaultDPoPProofValidator
{
public MockDPoPProofValidator(IdentityServerOptions options, IReplayCache replayCache, IClock clock, Microsoft.AspNetCore.DataProtection.IDataProtectionProvider dataProtectionProvider, ILogger<DefaultDPoPProofValidator> logger) : base(options, replayCache, clock, dataProtectionProvider, logger)
Expand Down Expand Up @@ -479,22 +524,18 @@ public async Task mtls_and_dpop_request_should_succeed()
// Set the client certificate in the pipeline
Pipeline.SetClientCertificate(clientCert);

// Act - Make a client credentials request using mTLS and DPoP
var tokenClient = Pipeline.GetMtlsClient();

var formParams = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", clientId },
{ "scope", "scope1" }
};

var form = new FormUrlEncodedContent(formParams);
tokenClient.DefaultRequestHeaders.Add("DPoP", CreateDPoPProofToken());
tokenClient.DefaultRequestHeaders.Add("DPoP", CreateDPoPProofToken(htu: IdentityServerPipeline.TokenMtlsEndpoint));

var response = await tokenClient.PostAsync(IdentityServerPipeline.TokenMtlsEndpoint, form);

// Assert
response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK);

var json = await response.Content.ReadAsStringAsync();
Expand Down
Loading
Loading