diff --git a/foss.slnx b/foss.slnx
index 2805c9c0..99a9b06a 100644
--- a/foss.slnx
+++ b/foss.slnx
@@ -50,6 +50,7 @@
+
diff --git a/identity-model/identity-model.slnf b/identity-model/identity-model.slnf
index 06ae3981..75986307 100644
--- a/identity-model/identity-model.slnf
+++ b/identity-model/identity-model.slnf
@@ -2,10 +2,13 @@
"solution": {
"path": "..\\foss.slnx",
"projects": [
+ "identity-model\\samples\\ClientAssertions\\ClientAssertions.csproj",
"identity-model\\samples\\HttpClientFactory\\HttpClientFactory.csproj",
"identity-model\\src\\IdentityModel\\IdentityModel.csproj",
"identity-model\\src\\TrimmableAnalysis\\TrimmableAnalysis.csproj",
- "identity-model\\test\\IdentityModel.Tests\\IdentityModel.Tests.csproj"
+ "identity-model\\test\\IdentityModel.Tests\\IdentityModel.Tests.csproj",
+ "identity-model-oidc-client\\src\\IdentityModel.OidcClient.Extensions\\IdentityModel.OidcClient.Extensions.csproj",
+ "identity-model-oidc-client\\src\\IdentityModel.OidcClient\\IdentityModel.OidcClient.csproj"
]
}
}
\ No newline at end of file
diff --git a/identity-model/samples/ClientAssertions/ClientAssertionService.cs b/identity-model/samples/ClientAssertions/ClientAssertionService.cs
new file mode 100644
index 00000000..c5c6fa57
--- /dev/null
+++ b/identity-model/samples/ClientAssertions/ClientAssertionService.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Duende Software. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+using Duende.IdentityModel;
+using Duende.IdentityModel.Client;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Microsoft.IdentityModel.Tokens;
+
+namespace ClientAssertions;
+
+///
+/// Creates signed client assertion JWTs (RFC 7523 / private_key_jwt).
+/// Each call to produces a JWT with a fresh
+/// jti and iat, which is critical when retries (e.g. DPoP nonce
+/// challenges) require a new assertion to avoid replay rejection.
+///
+public class ClientAssertionService
+{
+ private readonly string _clientId;
+ private readonly string _audience;
+ private readonly SigningCredentials _signingCredentials;
+
+ public ClientAssertionService(string clientId, string audience, SigningCredentials signingCredentials)
+ {
+ _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId));
+ _audience = audience ?? throw new ArgumentNullException(nameof(audience));
+ _signingCredentials = signingCredentials ?? throw new ArgumentNullException(nameof(signingCredentials));
+ }
+
+ ///
+ /// Creates a fresh with a unique jti.
+ ///
+ public Task CreateAssertionAsync()
+ {
+ var now = DateTime.UtcNow;
+
+ var descriptor = new SecurityTokenDescriptor
+ {
+ Issuer = _clientId,
+ Audience = _audience,
+ IssuedAt = now,
+ NotBefore = now,
+ Expires = now.AddMinutes(1),
+ SigningCredentials = _signingCredentials,
+ AdditionalHeaderClaims = new Dictionary
+ {
+ { "typ", "client-authentication+jwt" }
+ },
+ Claims = new Dictionary
+ {
+ { JwtClaimTypes.JwtId, Guid.NewGuid().ToString() },
+ { JwtClaimTypes.Subject, _clientId },
+ }
+ };
+
+ var handler = new JsonWebTokenHandler();
+ var jwt = handler.CreateToken(descriptor);
+
+ return Task.FromResult(new ClientAssertion
+ {
+ Type = OidcConstants.ClientAssertionTypes.JwtBearer,
+ Value = jwt
+ });
+ }
+
+ ///
+ /// Creates signing credentials from the RSA key pair that matches the public
+ /// key registered for the "m2m.jwt" client at demo.duendesoftware.com.
+ /// In production, load the private key from a secure store (e.g. Azure Key Vault).
+ ///
+ public static SigningCredentials CreateSigningCredentials()
+ {
+ // This JWK contains the private key that corresponds to the public key
+ // registered at the demo IdentityServer for the "m2m.jwt" client.
+ var jwk = """
+ {
+ "d":"GmiaucNIzdvsEzGjZjd43SDToy1pz-Ph-shsOUXXh-dsYNGftITGerp8bO1iryXh_zUEo8oDK3r1y4klTonQ6bLsWw4ogjLPmL3yiqsoSjJa1G2Ymh_RY_sFZLLXAcrmpbzdWIAkgkHSZTaliL6g57vA7gxvd8L4s82wgGer_JmURI0ECbaCg98JVS0Srtf9GeTRHoX4foLWKc1Vq6NHthzqRMLZe-aRBNU9IMvXNd7kCcIbHCM3GTD_8cFj135nBPP2HOgC_ZXI1txsEf-djqJj8W5vaM7ViKU28IDv1gZGH3CatoysYx6jv1XJVvb2PH8RbFKbJmeyUm3Wvo-rgQ",
+ "dp":"YNjVBTCIwZD65WCht5ve06vnBLP_Po1NtL_4lkholmPzJ5jbLYBU8f5foNp8DVJBdFQW7wcLmx85-NC5Pl1ZeyA-Ecbw4fDraa5Z4wUKlF0LT6VV79rfOF19y8kwf6MigyrDqMLcH_CRnRGg5NfDsijlZXffINGuxg6wWzhiqqE",
+ "dq":"LfMDQbvTFNngkZjKkN2CBh5_MBG6Yrmfy4kWA8IC2HQqID5FtreiY2MTAwoDcoINfh3S5CItpuq94tlB2t-VUv8wunhbngHiB5xUprwGAAnwJ3DL39D2m43i_3YP-UO1TgZQUAOh7Jrd4foatpatTvBtY3F1DrCrUKE5Kkn770M",
+ "e":"AQAB",
+ "kid":"ZzAjSnraU3bkWGnnAqLapYGpTyNfLbjbzgAPbbW2GEA",
+ "kty":"RSA",
+ "n":"wWwQFtSzeRjjerpEM5Rmqz_DsNaZ9S1Bw6UbZkDLowuuTCjBWUax0vBMMxdy6XjEEK4Oq9lKMvx9JzjmeJf1knoqSNrox3Ka0rnxXpNAz6sATvme8p9mTXyp0cX4lF4U2J54xa2_S9NF5QWvpXvBeC4GAJx7QaSw4zrUkrc6XyaAiFnLhQEwKJCwUw4NOqIuYvYp_IXhw-5Ti_icDlZS-282PcccnBeOcX7vc21pozibIdmZJKqXNsL1Ibx5Nkx1F1jLnekJAmdaACDjYRLL_6n3W4wUp19UvzB1lGtXcJKLLkqB6YDiZNu16OSiSprfmrRXvYmvD8m6Fnl5aetgKw",
+ "p":"7enorp9Pm9XSHaCvQyENcvdU99WCPbnp8vc0KnY_0g9UdX4ZDH07JwKu6DQEwfmUA1qspC-e_KFWTl3x0-I2eJRnHjLOoLrTjrVSBRhBMGEH5PvtZTTThnIY2LReH-6EhceGvcsJ_MhNDUEZLykiH1OnKhmRuvSdhi8oiETqtPE",
+ "q":"0CBLGi_kRPLqI8yfVkpBbA9zkCAshgrWWn9hsq6a7Zl2LcLaLBRUxH0q1jWnXgeJh9o5v8sYGXwhbrmuypw7kJ0uA3OgEzSsNvX5Ay3R9sNel-3Mqm8Me5OfWWvmTEBOci8RwHstdR-7b9ZT13jk-dsZI7OlV_uBja1ny9Nz9ts",
+ "qi":"pG6J4dcUDrDndMxa-ee1yG4KjZqqyCQcmPAfqklI2LmnpRIjcK78scclvpboI3JQyg6RCEKVMwAhVtQM6cBcIO3JrHgqeYDblp5wXHjto70HVW6Z8kBruNx1AH9E8LzNvSRL-JVTFzBkJuNgzKQfD0G77tQRgJ-Ri7qu3_9o1M4"
+ }
+ """;
+
+ var key = new JsonWebKey(jwk);
+ return new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
+ }
+}
diff --git a/identity-model/samples/ClientAssertions/ClientAssertions.csproj b/identity-model/samples/ClientAssertions/ClientAssertions.csproj
new file mode 100644
index 00000000..7962b279
--- /dev/null
+++ b/identity-model/samples/ClientAssertions/ClientAssertions.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net10.0
+ Exe
+ true
+ enable
+
+
+
+
+
+
+
+
diff --git a/identity-model/samples/ClientAssertions/Program.cs b/identity-model/samples/ClientAssertions/Program.cs
new file mode 100644
index 00000000..2aba5fe6
--- /dev/null
+++ b/identity-model/samples/ClientAssertions/Program.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Duende Software. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
+
+//
+// This sample demonstrates how to use a ClientAssertionFactory so that token
+// requests (including DPoP nonce retries) automatically produce a fresh
+// client_assertion JWT with a unique jti on each attempt.
+//
+// Background
+// ----------
+// When a token request carries a client_assertion (RFC 7521 / private_key_jwt),
+// the server may reject retries that reuse the same assertion because the jti
+// has already been seen. Setting ClientAssertionFactory on the ProtocolRequest
+// ensures that:
+// 1. The initial request gets a freshly-minted assertion.
+// 2. The factory is stored on HttpRequestMessage.Options so that downstream
+// handlers (e.g. DPoP proof-token handlers) can invoke it on retries.
+//
+
+using Duende.IdentityModel.Client;
+
+namespace ClientAssertions;
+
+public class Program
+{
+ private const string Authority = "https://demo.duendesoftware.com";
+ private const string TokenEndpoint = $"{Authority}/connect/token";
+ private const string ClientId = "m2m.jwt";
+ private const string Scope = "api";
+
+ public static async Task Main()
+ {
+ Console.WriteLine("+------------------------------------------+");
+ Console.WriteLine("| Client Assertions Sample |");
+ Console.WriteLine("+------------------------------------------+");
+ Console.WriteLine();
+
+ // 1. Create a signing key for client_assertion JWTs.
+ // In production this would be the key registered with the identity
+ // provider for private_key_jwt authentication.
+ var signingCredentials = ClientAssertionService.CreateSigningCredentials();
+
+ // 2. Create the assertion service.
+ var assertionService = new ClientAssertionService(ClientId, Authority, signingCredentials);
+
+ // 3. Build the HTTP client. In a real DPoP scenario you would wrap this
+ // with a ProofTokenMessageHandler, but this sample focuses on the
+ // client assertion factory pattern alone.
+ var client = new HttpClient();
+
+ // 4. Build the token request with a ClientAssertionFactory.
+ // The factory is called once for the initial request and stored on
+ // HttpRequestMessage.Options so that retry handlers can call it again.
+ var tokenRequest = new ClientCredentialsTokenRequest
+ {
+ Address = TokenEndpoint,
+ ClientId = ClientId,
+ ClientCredentialStyle = ClientCredentialStyle.PostBody,
+ Scope = Scope,
+
+ // The key feature: a factory that produces a fresh assertion each time.
+ ClientAssertionFactory = assertionService.CreateAssertionAsync,
+ };
+
+ Console.WriteLine("Requesting token with client_assertion...");
+ Console.WriteLine($" Endpoint : {TokenEndpoint}");
+ Console.WriteLine($" ClientId : {ClientId}");
+ Console.WriteLine();
+
+ // 5. Send the token request.
+ var response = await client.RequestClientCredentialsTokenAsync(tokenRequest);
+
+ // 6. Display the result.
+ if (response.IsError)
+ {
+ Console.WriteLine($"Error: {response.Error}");
+ Console.WriteLine($"Description: {response.ErrorDescription}");
+
+ if (response.HttpStatusCode != 0)
+ {
+ Console.WriteLine($"HTTP {(int)response.HttpStatusCode}");
+ }
+ }
+ else
+ {
+ Console.WriteLine("Success!");
+ Console.WriteLine($" Token type : {response.TokenType}");
+ Console.WriteLine($" Expires in : {response.ExpiresIn}s");
+ Console.WriteLine($" Access token : {response.AccessToken}");
+ }
+ }
+}
diff --git a/identity-model/src/IdentityModel/Client/HttpClientPushedAuthorizationExtensions.cs b/identity-model/src/IdentityModel/Client/HttpClientPushedAuthorizationExtensions.cs
index 8d389929..f6a7ffa1 100644
--- a/identity-model/src/IdentityModel/Client/HttpClientPushedAuthorizationExtensions.cs
+++ b/identity-model/src/IdentityModel/Client/HttpClientPushedAuthorizationExtensions.cs
@@ -66,6 +66,15 @@ public static Task PushAuthorizationAsync(this Http
internal static async Task PushAuthorizationAsync(this HttpMessageInvoker client, ProtocolRequest request, CancellationToken cancellationToken = default)
{
+ // If a factory is set, invoke it to get a fresh assertion for this attempt and
+ // store it on Options so that DPoP retry handlers can invoke it again on each
+ // subsequent attempt. The factory always takes precedence over a fixed ClientAssertion value.
+ if (request.ClientAssertionFactory != null)
+ {
+ request.ClientAssertion = await request.ClientAssertionFactory().ConfigureAwait();
+ request.Options.Set(ProtocolRequestOptions.ClientAssertionFactory, request.ClientAssertionFactory);
+ }
+
request.Prepare();
request.Method = HttpMethod.Post;
diff --git a/identity-model/src/IdentityModel/Client/HttpClientTokenRequestExtensions.cs b/identity-model/src/IdentityModel/Client/HttpClientTokenRequestExtensions.cs
index b4155031..209c5d14 100644
--- a/identity-model/src/IdentityModel/Client/HttpClientTokenRequestExtensions.cs
+++ b/identity-model/src/IdentityModel/Client/HttpClientTokenRequestExtensions.cs
@@ -213,6 +213,15 @@ public static async Task RequestTokenRawAsync(this HttpMessageInv
internal static async Task RequestTokenAsync(this HttpMessageInvoker client, ProtocolRequest request, CancellationToken cancellationToken = default)
{
+ // If a factory is set, invoke it to get a fresh assertion for this attempt and
+ // store it on Options so that DPoP retry handlers can invoke it again on each
+ // subsequent attempt. The factory always takes precedence over a fixed ClientAssertion value.
+ if (request.ClientAssertionFactory != null)
+ {
+ request.ClientAssertion = await request.ClientAssertionFactory().ConfigureAwait();
+ request.Options.Set(ProtocolRequestOptions.ClientAssertionFactory, request.ClientAssertionFactory);
+ }
+
request.Prepare();
request.Method = HttpMethod.Post;
diff --git a/identity-model/src/IdentityModel/Client/Messages/ProtocolRequest.cs b/identity-model/src/IdentityModel/Client/Messages/ProtocolRequest.cs
index 4e589a1c..dc5a5365 100644
--- a/identity-model/src/IdentityModel/Client/Messages/ProtocolRequest.cs
+++ b/identity-model/src/IdentityModel/Client/Messages/ProtocolRequest.cs
@@ -6,6 +6,21 @@
namespace Duende.IdentityModel.Client;
+///
+/// Well-known keys used by Duende IdentityModel
+/// when passing data through handler chains.
+///
+public static class ProtocolRequestOptions
+{
+ ///
+ /// The key used to store a on
+ /// . DPoP retry handlers read this key to obtain
+ /// a fresh for each attempt.
+ ///
+ public static readonly HttpRequestOptionsKey>?> ClientAssertionFactory =
+ new HttpRequestOptionsKey>?>("Duende.IdentityModel.ClientAssertionFactory");
+}
+
///
/// Models a base OAuth/OIDC request with client credentials
///
@@ -73,6 +88,14 @@ public ProtocolRequest()
///
public string? DPoPProofToken { get; set; }
+ ///
+ /// Gets or sets a factory function that creates a fresh on demand.
+ /// When set, this factory is stored on so that DPoP retry
+ /// handlers can invoke it to obtain a new assertion (with a fresh jti and iat) on
+ /// each attempt, avoiding client-assertion replay rejected by servers that enforce uniqueness.
+ ///
+ public Func>? ClientAssertionFactory { get; set; }
+
///
/// Gets or sets additional protocol parameters.
///
@@ -101,6 +124,7 @@ public T Clone()
Address = Address,
AuthorizationHeaderStyle = AuthorizationHeaderStyle,
ClientAssertion = ClientAssertion,
+ ClientAssertionFactory = ClientAssertionFactory,
ClientCredentialStyle = ClientCredentialStyle,
ClientId = ClientId,
ClientSecret = ClientSecret,
diff --git a/identity-model/test/IdentityModel.Tests/HttpClientExtensions/PushedAuthorizationTests.cs b/identity-model/test/IdentityModel.Tests/HttpClientExtensions/PushedAuthorizationTests.cs
index 4f2557cf..50bc0cc4 100644
--- a/identity-model/test/IdentityModel.Tests/HttpClientExtensions/PushedAuthorizationTests.cs
+++ b/identity-model/test/IdentityModel.Tests/HttpClientExtensions/PushedAuthorizationTests.cs
@@ -213,4 +213,65 @@ public async Task Pushed_authorization_with_request_uri_should_fail()
var exception = await act.ShouldThrowAsync();
exception.ParamName.ShouldBe("request_uri");
}
+
+ [Fact]
+ public async Task ClientAssertionFactoryShouldBeInvokedAndResultSentInBody()
+ {
+ var document = File.ReadAllText(FileName.Create("success_par_response.json"));
+ var handler = new NetworkHandler(document, HttpStatusCode.OK);
+ var client = new HttpClient(handler);
+
+ var factoryCallCount = 0;
+
+ var response = await client.PushAuthorizationAsync(new PushedAuthorizationRequest
+ {
+ Address = Endpoint,
+ ClientId = "client",
+ ResponseType = "code",
+ ClientCredentialStyle = ClientCredentialStyle.PostBody,
+ ClientAssertionFactory = () =>
+ {
+ factoryCallCount++;
+ return Task.FromResult(new ClientAssertion
+ {
+ Type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+ Value = "fresh-jwt-value"
+ });
+ }
+ }, _ct);
+
+ factoryCallCount.ShouldBe(1);
+
+ var fields = QueryHelpers.ParseQuery(handler.Body);
+ fields["client_assertion_type"].First().ShouldBe("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+ fields["client_assertion"].First().ShouldBe("fresh-jwt-value");
+ }
+
+ [Fact]
+ public async Task ClientAssertionFactoryShouldBeStoredOnRequestOptions()
+ {
+ var document = File.ReadAllText(FileName.Create("success_par_response.json"));
+ var handler = new NetworkHandler(document, HttpStatusCode.OK);
+ var client = new HttpClient(handler);
+
+ Func> factory = () => Task.FromResult(new ClientAssertion
+ {
+ Type = "type",
+ Value = "value"
+ });
+
+ await client.PushAuthorizationAsync(new PushedAuthorizationRequest
+ {
+ Address = Endpoint,
+ ClientId = "client",
+ ResponseType = "code",
+ ClientCredentialStyle = ClientCredentialStyle.PostBody,
+ ClientAssertionFactory = factory
+ }, _ct);
+
+ handler.Request.Options
+ .TryGetValue(ProtocolRequestOptions.ClientAssertionFactory, out var storedFactory)
+ .ShouldBeTrue();
+ storedFactory.ShouldBe(factory);
+ }
}
diff --git a/identity-model/test/IdentityModel.Tests/HttpClientExtensions/TokenRequestExtensionsRequestTests.cs b/identity-model/test/IdentityModel.Tests/HttpClientExtensions/TokenRequestExtensionsRequestTests.cs
index 79af5596..ebba9878 100644
--- a/identity-model/test/IdentityModel.Tests/HttpClientExtensions/TokenRequestExtensionsRequestTests.cs
+++ b/identity-model/test/IdentityModel.Tests/HttpClientExtensions/TokenRequestExtensionsRequestTests.cs
@@ -718,4 +718,131 @@ public async Task Setting_assertion_without_client_id_and_authz_header_should_ha
fields["client_assertion_type"].First().ShouldBe("type");
fields["client_assertion"].First().ShouldBe("value");
}
+
+ [Fact]
+ public async Task ClientAssertionFactoryShouldBeInvokedAndResultSentInBody()
+ {
+ var factoryCallCount = 0;
+
+ var response = await _client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
+ {
+ ClientId = "client",
+ ClientCredentialStyle = ClientCredentialStyle.PostBody,
+ Scope = "scope",
+ ClientAssertionFactory = () =>
+ {
+ factoryCallCount++;
+ return Task.FromResult(new ClientAssertion
+ {
+ Type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+ Value = "fresh-jwt-value"
+ });
+ }
+ }, _ct);
+
+ factoryCallCount.ShouldBe(1);
+
+ var fields = QueryHelpers.ParseQuery(_handler.Body);
+ fields["client_assertion_type"].First().ShouldBe("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+ fields["client_assertion"].First().ShouldBe("fresh-jwt-value");
+ }
+
+ [Fact]
+ public async Task ClientAssertionFactoryShouldBeStoredOnRequestOptions()
+ {
+ Func> factory = () => Task.FromResult(new ClientAssertion
+ {
+ Type = "type",
+ Value = "value"
+ });
+
+ await _client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
+ {
+ ClientId = "client",
+ ClientCredentialStyle = ClientCredentialStyle.PostBody,
+ ClientAssertionFactory = factory
+ }, _ct);
+
+ _handler.Request.Options
+ .TryGetValue(ProtocolRequestOptions.ClientAssertionFactory, out var storedFactory)
+ .ShouldBeTrue();
+ storedFactory.ShouldBe(factory);
+ }
+
+ [Fact]
+ public async Task ClientAssertionFactoryShouldTakePrecedenceOverFixedAssertion()
+ {
+ await _client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
+ {
+ ClientId = "client",
+ ClientCredentialStyle = ClientCredentialStyle.PostBody,
+ ClientAssertion = { Type = "fixed-type", Value = "fixed-value" },
+ ClientAssertionFactory = () => Task.FromResult(new ClientAssertion
+ {
+ Type = "factory-type",
+ Value = "factory-value"
+ })
+ }, _ct);
+
+ var fields = QueryHelpers.ParseQuery(_handler.Body);
+ fields["client_assertion_type"].First().ShouldBe("factory-type");
+ fields["client_assertion"].First().ShouldBe("factory-value");
+ }
+
+ [Fact]
+ public async Task NullClientAssertionFactoryShouldNotAffectFixedAssertion()
+ {
+ await _client.RequestTokenAsync(new TokenRequest
+ {
+ GrantType = "test",
+ ClientId = "client",
+ ClientCredentialStyle = ClientCredentialStyle.PostBody,
+ ClientAssertion = { Type = "fixed-type", Value = "fixed-value" },
+ ClientAssertionFactory = null
+ }, _ct);
+
+ var fields = QueryHelpers.ParseQuery(_handler.Body);
+ fields["client_assertion_type"].First().ShouldBe("fixed-type");
+ fields["client_assertion"].First().ShouldBe("fixed-value");
+
+ _handler.Request.Options
+ .TryGetValue(ProtocolRequestOptions.ClientAssertionFactory, out _)
+ .ShouldBeFalse();
+ }
+
+ [Fact]
+ public void CloneShouldCopyClientAssertionFactory()
+ {
+ Func> factory = () => Task.FromResult(new ClientAssertion
+ {
+ Type = "type",
+ Value = "value"
+ });
+
+ var original = new TokenRequest
+ {
+ GrantType = "test",
+ ClientId = "client",
+ ClientAssertionFactory = factory
+ };
+
+ var clone = original.Clone();
+
+ clone.ClientAssertionFactory.ShouldBe(factory);
+ }
+
+ [Fact]
+ public void CloneShouldCopyNullClientAssertionFactory()
+ {
+ var original = new TokenRequest
+ {
+ GrantType = "test",
+ ClientId = "client",
+ ClientAssertionFactory = null
+ };
+
+ var clone = original.Clone();
+
+ clone.ClientAssertionFactory.ShouldBeNull();
+ }
}
diff --git a/identity-model/test/IdentityModel.Tests/Verifications/PublicApiVerificationTests.VerifyPublicApi.verified.txt b/identity-model/test/IdentityModel.Tests/Verifications/PublicApiVerificationTests.VerifyPublicApi.verified.txt
index 600dbc10..d6f4a344 100644
--- a/identity-model/test/IdentityModel.Tests/Verifications/PublicApiVerificationTests.VerifyPublicApi.verified.txt
+++ b/identity-model/test/IdentityModel.Tests/Verifications/PublicApiVerificationTests.VerifyPublicApi.verified.txt
@@ -1159,6 +1159,7 @@ namespace Duende.IdentityModel.Client
public string? Address { get; set; }
public Duende.IdentityModel.Client.BasicAuthenticationHeaderStyle AuthorizationHeaderStyle { get; set; }
public Duende.IdentityModel.Client.ClientAssertion ClientAssertion { get; set; }
+ public System.Func>? ClientAssertionFactory { get; set; }
public Duende.IdentityModel.Client.ClientCredentialStyle ClientCredentialStyle { get; set; }
public string ClientId { get; set; }
public string? ClientSecret { get; set; }
@@ -1169,6 +1170,10 @@ namespace Duende.IdentityModel.Client
where T : Duende.IdentityModel.Client.ProtocolRequest, new () { }
public void Prepare() { }
}
+ public static class ProtocolRequestOptions
+ {
+ public static readonly System.Net.Http.HttpRequestOptionsKey>?> ClientAssertionFactory;
+ }
public class ProtocolResponse : System.IDisposable
{
public ProtocolResponse() { }
@@ -1484,4 +1489,4 @@ namespace Duende.IdentityModel.Validation
{
void Validate(string rawJwtResponse);
}
-}
+}
\ No newline at end of file