Skip to content

Commit d500a9a

Browse files
committed
Refresh Client Assertion on DPOP Nonce failure
1 parent 1c74e3d commit d500a9a

File tree

7 files changed

+238
-13
lines changed

7 files changed

+238
-13
lines changed

access-token-management/src/AccessTokenManagement.OpenIdConnect/Internal/AuthorizationServerDPoPHandler.cs

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
using System.Net;
55
using Duende.AccessTokenManagement.DPoP;
66
using Duende.AccessTokenManagement.Internal;
7-
7+
using Duende.IdentityModel;
88
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.WebUtilities;
10+
using Microsoft.Extensions.DependencyInjection;
911
using Microsoft.Extensions.Logging;
1012

1113
namespace Duende.AccessTokenManagement.OpenIdConnect.Internal;
@@ -40,7 +42,8 @@ internal class AuthorizationServerDPoPHandler(
4042
// We depend on the logger factory, rather than the logger itself, since
4143
// the type parameter of the logger (referencing this class) will not
4244
// always be accessible.
43-
private readonly ILogger<AuthorizationServerDPoPHandler> _logger = loggerFactory.CreateLogger<AuthorizationServerDPoPHandler>();
45+
private readonly ILogger<AuthorizationServerDPoPHandler> _logger =
46+
loggerFactory.CreateLogger<AuthorizationServerDPoPHandler>();
4447

4548
/// <inheritdoc/>
4649
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
@@ -87,6 +90,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
8790
_logger.DPoPErrorDuringTokenRefreshWillRetryWithServerNonce(LogLevel.Debug, response.GetDPoPError());
8891
response.Dispose();
8992
await SetDPoPProofTokenForCodeExchangeAsync(request, dPoPNonce, codeExchangeJwk).ConfigureAwait(false);
93+
await RefreshClientAssertionAsync(request).ConfigureAwait(false);
9094
return await base.SendAsync(request, ct).ConfigureAwait(false);
9195
}
9296
}
@@ -108,10 +112,8 @@ await dPoPNonceStore.StoreNonceAsync(new DPoPNonceContext
108112
return response;
109113
}
110114

111-
/// <summary>
112-
/// Creates a DPoP proof token and attaches it to a request.
113-
/// </summary>
114-
internal async Task SetDPoPProofTokenForCodeExchangeAsync(HttpRequestMessage request, DPoPNonce? dpopNonce = null, DPoPProofKey? jwk = null)
115+
internal async Task SetDPoPProofTokenForCodeExchangeAsync(HttpRequestMessage request, DPoPNonce? dpopNonce = null,
116+
DPoPProofKey? jwk = null)
115117
{
116118
if (jwk == null)
117119
{
@@ -138,4 +140,56 @@ internal async Task SetDPoPProofTokenForCodeExchangeAsync(HttpRequestMessage req
138140
_logger.FailedToCreateDPopProofToken(LogLevel.Debug, request.RequestUri);
139141
}
140142
}
143+
144+
private async Task RefreshClientAssertionAsync(HttpRequestMessage request)
145+
{
146+
if (request.Content == null)
147+
{
148+
return;
149+
}
150+
151+
var body = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
152+
var fields = QueryHelpers.ParseQuery(body);
153+
154+
if (!fields.ContainsKey(OidcConstants.TokenRequest.ClientAssertion))
155+
{
156+
return;
157+
}
158+
159+
var assertionService = httpContextAccessor.HttpContext?.RequestServices
160+
.GetService<IClientAssertionService>();
161+
if (assertionService == null)
162+
{
163+
return;
164+
}
165+
166+
var assertion = await assertionService.GetClientAssertionAsync().ConfigureAwait(false);
167+
if (assertion == null)
168+
{
169+
return;
170+
}
171+
172+
var pairs = new List<KeyValuePair<string, string>>();
173+
foreach (var kvp in fields)
174+
{
175+
var key = kvp.Key;
176+
switch (key)
177+
{
178+
case OidcConstants.TokenRequest.ClientAssertion:
179+
pairs.Add(new KeyValuePair<string, string>(key, assertion.Value));
180+
break;
181+
case OidcConstants.TokenRequest.ClientAssertionType:
182+
pairs.Add(new KeyValuePair<string, string>(key, assertion.Type));
183+
break;
184+
default:
185+
{
186+
pairs.AddRange(kvp.Value.Select(val => new KeyValuePair<string, string>(key, val ?? string.Empty)));
187+
188+
break;
189+
}
190+
}
191+
}
192+
193+
request.Content = new FormUrlEncodedContent(pairs);
194+
}
141195
}

access-token-management/src/AccessTokenManagement.OpenIdConnect/Internal/ConfigureOpenIdConnectOptions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ Task Callback(TokenValidatedContext context)
139139
// // and defer to the host and/or IUserTokenStore implementation to decide where the key is kept
140140
// //context.Properties!.RemoveProofKey();
141141
//}
142-
143142
return result;
144143
}
145144

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using System.Net.Http.Json;
5+
using System.Text.Json;
6+
using Duende.AccessTokenManagement.DPoP;
7+
using Duende.AccessTokenManagement.Framework;
8+
using Duende.IdentityModel;
9+
using Duende.IdentityModel.Client;
10+
using Duende.IdentityServer;
11+
using Duende.IdentityServer.Models;
12+
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
13+
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.IdentityModel.JsonWebTokens;
15+
using Microsoft.IdentityModel.Tokens;
16+
using JsonWebKey = Microsoft.IdentityModel.Tokens.JsonWebKey;
17+
18+
namespace Duende.AccessTokenManagement;
19+
20+
public sealed class DPoPWithClientAssertionsTests : IntegrationTestBase
21+
{
22+
private readonly CancellationToken _ct = TestContext.Current.CancellationToken;
23+
24+
public DPoPWithClientAssertionsTests(ITestOutputHelper output)
25+
: base(output, "dpop",
26+
configureUserTokenManagementOptions: opt => { opt.DPoPJsonWebKey = DPoPProofKey.Parse(DPoPPrivateJwk); },
27+
configureServicesBeforeAtm: services =>
28+
{
29+
services.AddSingleton<IClientAssertionService>(
30+
new JwtClientAssertionService(ClientAssertionCredentials));
31+
32+
services.Configure<OpenIdConnectOptions>("oidc", opt =>
33+
{
34+
opt.Events.OnAuthorizationCodeReceived = async context =>
35+
{
36+
var svc = context.HttpContext.RequestServices
37+
.GetRequiredService<IClientAssertionService>();
38+
var assertion = await svc.GetClientAssertionAsync(
39+
ClientCredentialsClientName.Parse("dpop"))
40+
?? throw new InvalidOperationException("Client assertion is null");
41+
42+
context.TokenEndpointRequest!.ClientAssertionType = assertion.Type;
43+
context.TokenEndpointRequest.ClientAssertion = assertion.Value;
44+
};
45+
});
46+
})
47+
{
48+
AppHost.ClientSecret = null;
49+
50+
var dpopClient = IdentityServerHost.Clients.Single(c => c.ClientId == "dpop");
51+
dpopClient.ClientSecrets.Clear();
52+
dpopClient.ClientSecrets.Add(new Secret
53+
{
54+
Type = IdentityServerConstants.SecretTypes.JsonWebKey,
55+
Value = BuildPublicJwk(ClientAssertionPrivateJwk)
56+
});
57+
}
58+
59+
[Fact]
60+
public async Task LoginWithDPoPAndClientAssertionsShouldSucceed()
61+
{
62+
await InitializeAsync();
63+
64+
await AppHost.LoginAsync("alice");
65+
66+
var codeExchangeRequest = IdentityServerHost.CapturedTokenRequests
67+
.FirstOrDefault(r => r.TryGetValue("grant_type", out var gt) && gt == "authorization_code");
68+
codeExchangeRequest.ShouldNotBeNull();
69+
codeExchangeRequest.ShouldContainKeyAndValue(OidcConstants.TokenRequest.ClientAssertionType,
70+
OidcConstants.ClientAssertionTypes.JwtBearer);
71+
codeExchangeRequest.ShouldContainKey(OidcConstants.TokenRequest.ClientAssertion);
72+
73+
// Verify we got a DPoP token back (confirms the DPoP proof was also accepted)
74+
var response = await AppHost.BrowserClient.GetAsync(AppHost.Url("/user_token"), _ct);
75+
var token = await response.Content.ReadFromJsonAsync<UserTokenModel>(_ct);
76+
token.ShouldNotBeNull();
77+
token.AccessTokenType.ShouldBe("DPoP");
78+
}
79+
80+
private const string ClientAssertionPrivateJwk =
81+
"""
82+
{
83+
"d":"GmiaucNIzdvsEzGjZjd43SDToy1pz-Ph-shsOUXXh-dsYNGftITGerp8bO1iryXh_zUEo8oDK3r1y4klTonQ6bLsWw4ogjLPmL3yiqsoSjJa1G2Ymh_RY_sFZLLXAcrmpbzdWIAkgkHSZTaliL6g57vA7gxvd8L4s82wgGer_JmURI0ECbaCg98JVS0Srtf9GeTRHoX4foLWKc1Vq6NHthzqRMLZe-aRBNU9IMvXNd7kCcIbHCM3GTD_8cFj135nBPP2HOgC_ZXI1txsEf-djqJj8W5vaM7ViKU28IDv1gZGH3CatoysYx6jv1XJVvb2PH8RbFKbJmeyUm3Wvo-rgQ",
84+
"dp":"YNjVBTCIwZD65WCht5ve06vnBLP_Po1NtL_4lkholmPzJ5jbLYBU8f5foNp8DVJBdFQW7wcLmx85-NC5Pl1ZeyA-Ecbw4fDraa5Z4wUKlF0LT6VV79rfOF19y8kwf6MigyrDqMLcH_CRnRGg5NfDsijlZXffINGuxg6wWzhiqqE",
85+
"dq":"LfMDQbvTFNngkZjKkN2CBh5_MBG6Yrmfy4kWA8IC2HQqID5FtreiY2MTAwoDcoINfh3S5CItpuq94tlB2t-VUv8wunhbngHiB5xUprwGAAnwJ3DL39D2m43i_3YP-UO1TgZQUAOh7Jrd4foatpatTvBtY3F1DrCrUKE5Kkn770M",
86+
"e":"AQAB",
87+
"kid":"ZzAjSnraU3bkWGnnAqLapYGpTyNfLbjbzgAPbbW2GEA",
88+
"kty":"RSA",
89+
"n":"wWwQFtSzeRjjerpEM5Rmqz_DsNaZ9S1Bw6UbZkDLowuuTCjBWUax0vBMMxdy6XjEEK4Oq9lKMvx9JzjmeJf1knoqSNrox3Ka0rnxXpNAz6sATvme8p9mTXyp0cX4lF4U2J54xa2_S9NF5QWvpXvBeC4GAJx7QaSw4zrUkrc6XyaAiFnLhQEwKJCwUw4NOqIuYvYp_IXhw-5Ti_icDlZS-282PcccnBeOcX7vc21pozibIdmZJKqXNsL1Ibx5Nkx1F1jLnekJAmdaACDjYRLL_6n3W4wUp19UvzB1lGtXcJKLLkqB6YDiZNu16OSiSprfmrRXvYmvD8m6Fnl5aetgKw",
90+
"p":"7enorp9Pm9XSHaCvQyENcvdU99WCPbnp8vc0KnY_0g9UdX4ZDH07JwKu6DQEwfmUA1qspC-e_KFWTl3x0-I2eJRnHjLOoLrTjrVSBRhBMGEH5PvtZTTThnIY2LReH-6EhceGvcsJ_MhNDUEZLykiH1OnKhmRuvSdhi8oiETqtPE",
91+
"q":"0CBLGi_kRPLqI8yfVkpBbA9zkCAshgrWWn9hsq6a7Zl2LcLaLBRUxH0q1jWnXgeJh9o5v8sYGXwhbrmuypw7kJ0uA3OgEzSsNvX5Ay3R9sNel-3Mqm8Me5OfWWvmTEBOci8RwHstdR-7b9ZT13jk-dsZI7OlV_uBja1ny9Nz9ts",
92+
"qi":"pG6J4dcUDrDndMxa-ee1yG4KjZqqyCQcmPAfqklI2LmnpRIjcK78scclvpboI3JQyg6RCEKVMwAhVtQM6cBcIO3JrHgqeYDblp5wXHjto70HVW6Z8kBruNx1AH9E8LzNvSRL-JVTFzBkJuNgzKQfD0G77tQRgJ-Ri7qu3_9o1M4"
93+
}
94+
""";
95+
96+
private const string DPoPPrivateJwk =
97+
"""
98+
{
99+
"kty":"RSA",
100+
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
101+
"e":"AQAB",
102+
"d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q",
103+
"p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs",
104+
"q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk",
105+
"dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0",
106+
"dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk",
107+
"qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU",
108+
"alg":"RS256",
109+
"kid":"2011-04-29"
110+
}
111+
""";
112+
113+
private static readonly SigningCredentials ClientAssertionCredentials =
114+
new(new JsonWebKey(ClientAssertionPrivateJwk), "RS256");
115+
116+
117+
private static string BuildPublicJwk(string privateJwk)
118+
{
119+
var fullKey = new JsonWebKey(privateJwk);
120+
var publicKey = new JsonWebKey
121+
{
122+
Kty = fullKey.Kty,
123+
N = fullKey.N,
124+
E = fullKey.E,
125+
Kid = fullKey.Kid,
126+
Alg = "RS256"
127+
};
128+
return JsonSerializer.Serialize(publicKey);
129+
}
130+
}
131+
132+
internal sealed class JwtClientAssertionService(SigningCredentials credentials) : IClientAssertionService
133+
{
134+
public Task<ClientAssertion?> GetClientAssertionAsync(
135+
ClientCredentialsClientName? clientName = null,
136+
TokenRequestParameters? parameters = null,
137+
CancellationToken ct = default)
138+
{
139+
var descriptor = new SecurityTokenDescriptor
140+
{
141+
Issuer = "dpop",
142+
Audience = "https://identityserver/connect/token",
143+
Expires = DateTime.UtcNow.AddMinutes(1),
144+
SigningCredentials = credentials,
145+
Claims = new Dictionary<string, object>
146+
{
147+
{ JwtClaimTypes.JwtId, Guid.NewGuid().ToString() },
148+
{ JwtClaimTypes.Subject, "dpop" },
149+
{ JwtClaimTypes.IssuedAt, DateTimeOffset.UtcNow.ToUnixTimeSeconds() }
150+
}
151+
};
152+
153+
var jwt = new JsonWebTokenHandler().CreateToken(descriptor);
154+
155+
return Task.FromResult<ClientAssertion?>(new ClientAssertion
156+
{
157+
Type = OidcConstants.ClientAssertionTypes.JwtBearer,
158+
Value = jwt
159+
});
160+
}
161+
}

access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,24 @@ public class AppHost : GenericHost
2222
private readonly IdentityServerHost _identityServerHost;
2323
private readonly ApiHost _apiHost;
2424
private readonly Action<UserTokenManagementOptions>? _configureUserTokenManagementOptions;
25+
private readonly Action<IServiceCollection>? _configureServicesBeforeAtm;
2526

2627
public AppHost(
2728
WriteTestOutput writeTestOutput,
2829
IdentityServerHost identityServerHost,
2930
ApiHost apiHost,
3031
string clientId,
3132
string baseAddress = "https://app",
32-
Action<UserTokenManagementOptions>? configureUserTokenManagementOptions = null)
33+
Action<UserTokenManagementOptions>? configureUserTokenManagementOptions = null,
34+
Action<IServiceCollection>? configureServicesBeforeAtm = null)
3335
: base(writeTestOutput, baseAddress)
3436
{
3537
_identityServerHost = identityServerHost;
3638
_apiHost = apiHost;
3739
ClientId = clientId;
3840
ClientSecret = "secret";
3941
_configureUserTokenManagementOptions = configureUserTokenManagementOptions;
42+
_configureServicesBeforeAtm = configureServicesBeforeAtm;
4043
OnConfigureServices += ConfigureServices;
4144
OnConfigure += Configure;
4245
}
@@ -106,6 +109,8 @@ private void ConfigureServices(IServiceCollection services)
106109
options.ProtocolValidator.RequireNonce = false;
107110
});
108111

112+
_configureServicesBeforeAtm?.Invoke(services);
113+
109114
services.AddOpenIdConnectAccessTokenManagement(opt =>
110115
{
111116
opt.UseChallengeSchemeScopedTokens = true;

access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ private void ConfigureServices(IServiceCollection services)
6666
.AddInMemoryClients(Clients)
6767
.AddInMemoryIdentityResources(IdentityResources)
6868
.AddInMemoryApiResources(ApiResources)
69-
.AddInMemoryApiScopes(ApiScopes);
69+
.AddInMemoryApiScopes(ApiScopes)
70+
.AddJwtBearerClientAuthentication();
7071
}
7172

7273
private void Configure(IApplicationBuilder app)

access-token-management/test/AccessTokenManagement.Tests/Framework/IntegrationTestBase.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Duende.AccessTokenManagement.OpenIdConnect;
55
using Duende.IdentityModel;
66
using Duende.IdentityServer.Models;
7+
using Microsoft.Extensions.DependencyInjection;
78

89
namespace Duende.AccessTokenManagement.Framework;
910

@@ -16,7 +17,11 @@ public abstract class IntegrationTestBase : IAsyncDisposable
1617
protected readonly ApiHost ApiHost;
1718
protected readonly AppHost AppHost;
1819

19-
protected IntegrationTestBase(ITestOutputHelper output, string clientId = "web", Action<UserTokenManagementOptions>? configureUserTokenManagementOptions = null)
20+
protected IntegrationTestBase(
21+
ITestOutputHelper output,
22+
string clientId = "web",
23+
Action<UserTokenManagementOptions>? configureUserTokenManagementOptions = null,
24+
Action<IServiceCollection>? configureServicesBeforeAtm = null)
2025
{
2126
IdentityServerHost = new IdentityServerHost(output.WriteLine);
2227

@@ -70,7 +75,7 @@ protected IntegrationTestBase(ITestOutputHelper output, string clientId = "web",
7075
});
7176

7277
ApiHost = new ApiHost(output.WriteLine, IdentityServerHost, ["scope1", "scope2"]);
73-
AppHost = new AppHost(output.WriteLine, IdentityServerHost, ApiHost, clientId, configureUserTokenManagementOptions: configureUserTokenManagementOptions);
78+
AppHost = new AppHost(output.WriteLine, IdentityServerHost, ApiHost, clientId, configureUserTokenManagementOptions: configureUserTokenManagementOptions, configureServicesBeforeAtm: configureServicesBeforeAtm);
7479
}
7580

7681
public virtual async ValueTask DisposeAsync()

access-token-management/test/AccessTokenManagement.Tests/UserTokenManagementWithDPoPTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public async Task dpop_token_refresh_should_succeed()
3939
// The DPoP proof token is valid for 1 second, and that validity is checked with the server nonce.
4040
// We have to wait 2 seconds to make sure our previous (from the initial login) nonce is no longer
4141
// valid. Ideally we would verify that we actually retried, but in this test we aren't mocking
42-
// the http client so there isn't an obvious way to do that. However, the next test
42+
// the http client so there isn't an obvious way to do that. However, the next test
4343
// (dpop_nonce_is_respected_during_code_exchange) does exactly that.
4444
await Task.Delay(2000, _ct);
4545

@@ -57,7 +57,7 @@ public async Task dpop_nonce_is_respected_during_code_exchange()
5757
var mockHttp = new MockHttpMessageHandler(BackendDefinitionBehavior.Always);
5858
AppHost.IdentityServerHttpHandler = mockHttp;
5959

60-
// Initial login request
60+
// Initial login request
6161
var initialTokenResponse = new
6262
{
6363
id_token = IdentityServerHost.CreateIdToken("1", "dpop"),

0 commit comments

Comments
 (0)