Skip to content

Commit 9cce966

Browse files
fixed the test clients
1 parent 44ae365 commit 9cce966

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+39906
-11
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 Duende.AccessTokenManagement;
5+
using Duende.AccessTokenManagement.OpenIdConnect;
6+
using Duende.IdentityModel;
7+
using Duende.IdentityModel.Client;
8+
using Microsoft.IdentityModel.JsonWebTokens;
9+
using Microsoft.IdentityModel.Tokens;
10+
11+
namespace WebClientAssertions;
12+
13+
/// <summary>
14+
/// Creates signed client assertion JWTs (RFC 7523 / private_key_jwt) for use
15+
/// with Duende's Access Token Management library.
16+
///
17+
/// Each call produces a JWT with a fresh <c>jti</c> and <c>iat</c>, which is
18+
/// critical when DPoP nonce retries require a new assertion to avoid replay
19+
/// rejection by the authorization server.
20+
/// </summary>
21+
public class ClientAssertionService : IClientAssertionService
22+
{
23+
/// <summary>
24+
/// RSA private key that matches the public key registered at
25+
/// demo.duendesoftware.com for the JWT client authentication clients.
26+
/// In production, load from a secure store (e.g. Azure Key Vault).
27+
/// </summary>
28+
private static readonly SigningCredentials Credential = new(
29+
new JsonWebKey("""
30+
{
31+
"d":"GmiaucNIzdvsEzGjZjd43SDToy1pz-Ph-shsOUXXh-dsYNGftITGerp8bO1iryXh_zUEo8oDK3r1y4klTonQ6bLsWw4ogjLPmL3yiqsoSjJa1G2Ymh_RY_sFZLLXAcrmpbzdWIAkgkHSZTaliL6g57vA7gxvd8L4s82wgGer_JmURI0ECbaCg98JVS0Srtf9GeTRHoX4foLWKc1Vq6NHthzqRMLZe-aRBNU9IMvXNd7kCcIbHCM3GTD_8cFj135nBPP2HOgC_ZXI1txsEf-djqJj8W5vaM7ViKU28IDv1gZGH3CatoysYx6jv1XJVvb2PH8RbFKbJmeyUm3Wvo-rgQ",
32+
"dp":"YNjVBTCIwZD65WCht5ve06vnBLP_Po1NtL_4lkholmPzJ5jbLYBU8f5foNp8DVJBdFQW7wcLmx85-NC5Pl1ZeyA-Ecbw4fDraa5Z4wUKlF0LT6VV79rfOF19y8kwf6MigyrDqMLcH_CRnRGg5NfDsijlZXffINGuxg6wWzhiqqE",
33+
"dq":"LfMDQbvTFNngkZjKkN2CBh5_MBG6Yrmfy4kWA8IC2HQqID5FtreiY2MTAwoDcoINfh3S5CItpuq94tlB2t-VUv8wunhbngHiB5xUprwGAAnwJ3DL39D2m43i_3YP-UO1TgZQUAOh7Jrd4foatpatTvBtY3F1DrCrUKE5Kkn770M",
34+
"e":"AQAB",
35+
"kid":"ZzAjSnraU3bkWGnnAqLapYGpTyNfLbjbzgAPbbW2GEA",
36+
"kty":"RSA",
37+
"n":"wWwQFtSzeRjjerpEM5Rmqz_DsNaZ9S1Bw6UbZkDLowuuTCjBWUax0vBMMxdy6XjEEK4Oq9lKMvx9JzjmeJf1knoqSNrox3Ka0rnxXpNAz6sATvme8p9mTXyp0cX4lF4U2J54xa2_S9NF5QWvpXvBeC4GAJx7QaSw4zrUkrc6XyaAiFnLhQEwKJCwUw4NOqIuYvYp_IXhw-5Ti_icDlZS-282PcccnBeOcX7vc21pozibIdmZJKqXNsL1Ibx5Nkx1F1jLnekJAmdaACDjYRLL_6n3W4wUp19UvzB1lGtXcJKLLkqB6YDiZNu16OSiSprfmrRXvYmvD8m6Fnl5aetgKw",
38+
"p":"7enorp9Pm9XSHaCvQyENcvdU99WCPbnp8vc0KnY_0g9UdX4ZDH07JwKu6DQEwfmUA1qspC-e_KFWTl3x0-I2eJRnHjLOoLrTjrVSBRhBMGEH5PvtZTTThnIY2LReH-6EhceGvcsJ_MhNDUEZLykiH1OnKhmRuvSdhi8oiETqtPE",
39+
"q":"0CBLGi_kRPLqI8yfVkpBbA9zkCAshgrWWn9hsq6a7Zl2LcLaLBRUxH0q1jWnXgeJh9o5v8sYGXwhbrmuypw7kJ0uA3OgEzSsNvX5Ay3R9sNel-3Mqm8Me5OfWWvmTEBOci8RwHstdR-7b9ZT13jk-dsZI7OlV_uBja1ny9Nz9ts",
40+
"qi":"pG6J4dcUDrDndMxa-ee1yG4KjZqqyCQcmPAfqklI2LmnpRIjcK78scclvpboI3JQyg6RCEKVMwAhVtQM6cBcIO3JrHgqeYDblp5wXHjto70HVW6Z8kBruNx1AH9E8LzNvSRL-JVTFzBkJuNgzKQfD0G77tQRgJ-Ri7qu3_9o1M4"
41+
}
42+
"""),
43+
SecurityAlgorithms.RsaSha256);
44+
45+
private const string Authority = "https://demo.duendesoftware.com";
46+
47+
public Task<ClientAssertion?> GetClientAssertionAsync(
48+
ClientCredentialsClientName? clientName = null,
49+
TokenRequestParameters? parameters = null,
50+
CancellationToken ct = default)
51+
{
52+
// Determine the client_id for the assertion's issuer/subject claims.
53+
// The library calls this with different clientName values depending on context:
54+
// - scheme-based name during OIDC flows (code exchange, refresh)
55+
// - the literal client name "m2m.jwt" for the named M2M client
56+
var clientId = ResolveClientId(clientName);
57+
58+
var now = DateTime.UtcNow;
59+
var descriptor = new SecurityTokenDescriptor
60+
{
61+
Issuer = clientId,
62+
Audience = Authority,
63+
IssuedAt = now,
64+
NotBefore = now,
65+
Expires = now.AddMinutes(1),
66+
SigningCredentials = Credential,
67+
68+
Claims = new Dictionary<string, object>
69+
{
70+
{ JwtClaimTypes.JwtId, Guid.NewGuid().ToString() },
71+
{ JwtClaimTypes.Subject, clientId },
72+
},
73+
74+
AdditionalHeaderClaims = new Dictionary<string, object>
75+
{
76+
{ "typ", "client-authentication+jwt" }
77+
}
78+
};
79+
80+
var handler = new JsonWebTokenHandler();
81+
var jwt = handler.CreateToken(descriptor);
82+
83+
return Task.FromResult<ClientAssertion?>(new ClientAssertion
84+
{
85+
Type = OidcConstants.ClientAssertionTypes.JwtBearer,
86+
Value = jwt
87+
});
88+
}
89+
90+
/// <summary>
91+
/// Maps the ATM client name to the actual OAuth client_id used in the assertion.
92+
/// </summary>
93+
private static string ResolveClientId(ClientCredentialsClientName? clientName)
94+
{
95+
var name = clientName?.ToString();
96+
97+
// Default / OIDC scheme-based client → use the interactive DPoP client
98+
if (string.IsNullOrEmpty(name) || name.Contains(OpenIdConnectTokenManagementDefaults.ClientCredentialsClientNamePrefix))
99+
{
100+
return "interactive.confidential.jwt.dpop";
101+
}
102+
103+
// Named M2M client
104+
return name switch
105+
{
106+
"m2m.jwt" => "m2m.jwt",
107+
_ => name
108+
};
109+
}
110+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.Text.Json;
5+
using Duende.AccessTokenManagement;
6+
using Duende.AccessTokenManagement.OpenIdConnect;
7+
using Duende.IdentityModel.Client;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Mvc;
10+
11+
namespace WebClientAssertions.Controllers;
12+
13+
public class HomeController : Controller
14+
{
15+
private readonly IHttpClientFactory _httpClientFactory;
16+
private readonly IUserTokenManager _tokenManager;
17+
18+
public HomeController(IHttpClientFactory httpClientFactory, IUserTokenManager tokenManager)
19+
{
20+
_httpClientFactory = httpClientFactory;
21+
_tokenManager = tokenManager;
22+
}
23+
24+
[AllowAnonymous]
25+
public IActionResult Index() => View();
26+
27+
public IActionResult Secure() => View();
28+
29+
public IActionResult Logout() => SignOut("cookie", "oidc");
30+
31+
// -----------------------------------------------------------------------
32+
// User token endpoints (DPoP + JWT client assertion)
33+
// -----------------------------------------------------------------------
34+
35+
public async Task<IActionResult> CallApiAsUserManual()
36+
{
37+
var token = await _tokenManager.GetAccessTokenAsync(User).GetToken();
38+
var client = _httpClientFactory.CreateClient();
39+
client.SetBearerToken(token.AccessToken.ToString()!);
40+
41+
var response = await client.GetStringAsync("https://demo.duendesoftware.com/api/dpop/test");
42+
ViewBag.Json = PrettyPrint(response);
43+
44+
return View("CallApi");
45+
}
46+
47+
public async Task<IActionResult> CallApiAsUserFactory()
48+
{
49+
var client = _httpClientFactory.CreateClient("user_client");
50+
var response = await client.GetStringAsync("test");
51+
52+
ViewBag.Json = PrettyPrint(response);
53+
return View("CallApi");
54+
}
55+
56+
public async Task<IActionResult> CallApiAsUserFactoryTyped([FromServices] TypedUserClient client)
57+
{
58+
var response = await client.CallApi();
59+
ViewBag.Json = PrettyPrint(response);
60+
61+
return View("CallApi");
62+
}
63+
64+
// -----------------------------------------------------------------------
65+
// Client token endpoints (M2M / client credentials + JWT assertion)
66+
// -----------------------------------------------------------------------
67+
68+
[AllowAnonymous]
69+
public async Task<IActionResult> CallApiAsClientFactory()
70+
{
71+
var client = _httpClientFactory.CreateClient("client");
72+
var response = await client.GetStringAsync("test");
73+
74+
ViewBag.Json = PrettyPrint(response);
75+
return View("CallApi");
76+
}
77+
78+
[AllowAnonymous]
79+
public async Task<IActionResult> CallApiAsClientFactoryTyped([FromServices] TypedClientClient client)
80+
{
81+
var response = await client.CallApi();
82+
ViewBag.Json = PrettyPrint(response);
83+
84+
return View("CallApi");
85+
}
86+
87+
private static string PrettyPrint(string json)
88+
{
89+
var doc = JsonDocument.Parse(json).RootElement;
90+
return JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
91+
}
92+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 Serilog;
5+
using Serilog.Events;
6+
using Serilog.Sinks.SystemConsole.Themes;
7+
using WebClientAssertions;
8+
9+
Log.Logger = new LoggerConfiguration()
10+
.WriteTo.Console()
11+
.CreateBootstrapLogger();
12+
13+
Log.Information("Host.Main Starting up");
14+
15+
Console.Title = "WebClientAssertions (Sample)";
16+
17+
try
18+
{
19+
var builder = WebApplication.CreateBuilder(args);
20+
21+
builder.Host.UseSerilog((ctx, lc) => lc
22+
.WriteTo.Console(
23+
outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}",
24+
theme: AnsiConsoleTheme.Code)
25+
.MinimumLevel.Information()
26+
.MinimumLevel.Override("Duende", LogEventLevel.Verbose)
27+
.Enrich.FromLogContext());
28+
29+
var app = builder
30+
.ConfigureServices()
31+
.ConfigurePipeline();
32+
33+
app.Run();
34+
}
35+
catch (Exception ex)
36+
{
37+
Log.Fatal(ex, "Unhandled exception");
38+
}
39+
finally
40+
{
41+
Log.Information("Shut down complete");
42+
Log.CloseAndFlush();
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"profiles": {
3+
"WebClientAssertions": {
4+
"commandName": "Project",
5+
"launchBrowser": true,
6+
"environmentVariables": {
7+
"ASPNETCORE_ENVIRONMENT": "Development"
8+
},
9+
"applicationUrl": "https://localhost:44305"
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)