Skip to content
This repository was archived by the owner on Feb 23, 2025. It is now read-only.

Commit dfb8097

Browse files
authored
Merge pull request #422 from IdentityModel/joe/client-assertion-event
Add function to options to create client assertions dynamically
2 parents eea4dd9 + b1605d9 commit dfb8097

File tree

5 files changed

+85
-5
lines changed

5 files changed

+85
-5
lines changed

Diff for: clients/ConsoleClientWithBrowser/Program.cs

+53-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
using System.Threading.Tasks;
88
using Microsoft.Extensions.Logging;
99
using Serilog.Sinks.SystemConsole.Themes;
10+
using System.IdentityModel.Tokens.Jwt;
11+
using System.Security.Claims;
12+
using IdentityModel;
13+
using Microsoft.IdentityModel.Tokens;
14+
using IdentityModel.Client;
1015

1116
namespace ConsoleClientWithBrowser
1217
{
@@ -29,17 +34,62 @@ public static async Task Main()
2934
await SignIn();
3035
}
3136

37+
private static string rsaKey =
38+
"{" +
39+
"\"d\":\"GmiaucNIzdvsEzGjZjd43SDToy1pz-Ph-shsOUXXh-dsYNGftITGerp8bO1iryXh_zUEo8oDK3r1y4klTonQ6bLsWw4ogjLPmL3yiqsoSjJa1G2Ymh_RY_sFZLLXAcrmpbzdWIAkgkHSZTaliL6g57vA7gxvd8L4s82wgGer_JmURI0ECbaCg98JVS0Srtf9GeTRHoX4foLWKc1Vq6NHthzqRMLZe-aRBNU9IMvXNd7kCcIbHCM3GTD_8cFj135nBPP2HOgC_ZXI1txsEf-djqJj8W5vaM7ViKU28IDv1gZGH3CatoysYx6jv1XJVvb2PH8RbFKbJmeyUm3Wvo-rgQ\"," +
40+
"\"dp\":\"YNjVBTCIwZD65WCht5ve06vnBLP_Po1NtL_4lkholmPzJ5jbLYBU8f5foNp8DVJBdFQW7wcLmx85-NC5Pl1ZeyA-Ecbw4fDraa5Z4wUKlF0LT6VV79rfOF19y8kwf6MigyrDqMLcH_CRnRGg5NfDsijlZXffINGuxg6wWzhiqqE\"," +
41+
"\"dq\":\"LfMDQbvTFNngkZjKkN2CBh5_MBG6Yrmfy4kWA8IC2HQqID5FtreiY2MTAwoDcoINfh3S5CItpuq94tlB2t-VUv8wunhbngHiB5xUprwGAAnwJ3DL39D2m43i_3YP-UO1TgZQUAOh7Jrd4foatpatTvBtY3F1DrCrUKE5Kkn770M\"," +
42+
"\"e\":\"AQAB\"," +
43+
"\"kid\":\"ZzAjSnraU3bkWGnnAqLapYGpTyNfLbjbzgAPbbW2GEA\"," +
44+
"\"kty\":\"RSA\"," +
45+
"\"n\":\"wWwQFtSzeRjjerpEM5Rmqz_DsNaZ9S1Bw6UbZkDLowuuTCjBWUax0vBMMxdy6XjEEK4Oq9lKMvx9JzjmeJf1knoqSNrox3Ka0rnxXpNAz6sATvme8p9mTXyp0cX4lF4U2J54xa2_S9NF5QWvpXvBeC4GAJx7QaSw4zrUkrc6XyaAiFnLhQEwKJCwUw4NOqIuYvYp_IXhw-5Ti_icDlZS-282PcccnBeOcX7vc21pozibIdmZJKqXNsL1Ibx5Nkx1F1jLnekJAmdaACDjYRLL_6n3W4wUp19UvzB1lGtXcJKLLkqB6YDiZNu16OSiSprfmrRXvYmvD8m6Fnl5aetgKw\"," +
46+
"\"p\":\"7enorp9Pm9XSHaCvQyENcvdU99WCPbnp8vc0KnY_0g9UdX4ZDH07JwKu6DQEwfmUA1qspC-e_KFWTl3x0-I2eJRnHjLOoLrTjrVSBRhBMGEH5PvtZTTThnIY2LReH-6EhceGvcsJ_MhNDUEZLykiH1OnKhmRuvSdhi8oiETqtPE\"," +
47+
"\"q\":\"0CBLGi_kRPLqI8yfVkpBbA9zkCAshgrWWn9hsq6a7Zl2LcLaLBRUxH0q1jWnXgeJh9o5v8sYGXwhbrmuypw7kJ0uA3OgEzSsNvX5Ay3R9sNel-3Mqm8Me5OfWWvmTEBOci8RwHstdR-7b9ZT13jk-dsZI7OlV_uBja1ny9Nz9ts\"," +
48+
"\"qi\":\"pG6J4dcUDrDndMxa-ee1yG4KjZqqyCQcmPAfqklI2LmnpRIjcK78scclvpboI3JQyg6RCEKVMwAhVtQM6cBcIO3JrHgqeYDblp5wXHjto70HVW6Z8kBruNx1AH9E8LzNvSRL-JVTFzBkJuNgzKQfD0G77tQRgJ-Ri7qu3_9o1M4\"" +
49+
"}";
50+
51+
private static string CreateClientToken(SigningCredentials credential, string clientId, string audience)
52+
{
53+
var now = DateTime.UtcNow;
54+
55+
var token = new JwtSecurityToken(
56+
clientId,
57+
audience,
58+
new List<Claim>()
59+
{
60+
new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()),
61+
new Claim(JwtClaimTypes.Subject, clientId),
62+
new Claim(JwtClaimTypes.IssuedAt, now.ToEpochTime().ToString(), ClaimValueTypes.Integer64)
63+
},
64+
now,
65+
now.AddMinutes(1),
66+
credential
67+
);
68+
69+
var tokenHandler = new JwtSecurityTokenHandler();
70+
return tokenHandler.WriteToken(token);
71+
}
72+
3273
private static async Task SignIn()
3374
{
3475
// create a redirect URI using an available port on the loopback address.
3576
// requires the OP to allow random ports on 127.0.0.1 - otherwise set a static port
3677
var browser = new SystemBrowser();
37-
string redirectUri = string.Format($"http://127.0.0.1:{browser.Port}");
78+
var redirectUri = string.Format($"http://127.0.0.1:{browser.Port}");
79+
var authority = "https://demo.duendesoftware.com";
80+
81+
var jwk = new JsonWebKey(rsaKey);
82+
var credential = new SigningCredentials(jwk, "RS256");
3883

3984
var options = new OidcClientOptions
4085
{
41-
Authority = "https://demo.duendesoftware.com",
42-
ClientId = "interactive.public.short",
86+
Authority = authority,
87+
ClientId = "interactive.confidential.short.jwt",
88+
GetClientAssertionAsync = () => Task.FromResult(new ClientAssertion
89+
{
90+
Type = OidcConstants.ClientAssertionTypes.JwtBearer,
91+
Value = CreateClientToken(credential, "interactive.confidential.short.jwt", authority)
92+
}),
4393
RedirectUri = redirectUri,
4494
Scope = "openid profile api offline_access",
4595
FilterClaims = false,

Diff for: src/OidcClient/OidcClient.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ public virtual async Task<RefreshTokenResult> RefreshTokenAsync(
336336
Address = Options.ProviderInformation.TokenEndpoint,
337337
ClientId = Options.ClientId,
338338
ClientSecret = Options.ClientSecret,
339-
ClientAssertion = Options.ClientAssertion,
339+
ClientAssertion = await Options.GetClientAssertionAsync(),
340340
ClientCredentialStyle = Options.TokenClientCredentialStyle,
341341
RefreshToken = refreshToken,
342342
Parameters = backChannelParameters,

Diff for: src/OidcClient/OidcClientOptions.cs

+16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
using System.Net.Http;
1111
using System.Text.Json.Serialization;
1212
using Microsoft.Extensions.Logging.Abstractions;
13+
using System.Threading.Tasks;
14+
using System.Runtime.CompilerServices;
1315

1416
namespace IdentityModel.OidcClient
1517
{
@@ -18,6 +20,14 @@ namespace IdentityModel.OidcClient
1820
/// </summary>
1921
public class OidcClientOptions
2022
{
23+
/// <summary>
24+
/// Creates an instance of the OidcClientOptions class.
25+
/// </summary>
26+
public OidcClientOptions()
27+
{
28+
GetClientAssertionAsync ??= () => Task.FromResult(ClientAssertion);
29+
}
30+
2131
/// <summary>
2232
/// Gets or sets the authority.
2333
/// </summary>
@@ -58,6 +68,12 @@ public class OidcClientOptions
5868
/// </value>
5969
public ClientAssertion ClientAssertion { get; set; } = new ClientAssertion();
6070

71+
/// <summary>
72+
/// Gets or sets a callback that computes the client assertion. By default, this returns the statically configured ClientAssertion
73+
/// </summary>
74+
[JsonIgnore]
75+
public Func<Task<ClientAssertion>> GetClientAssertionAsync { get; set; }
76+
6177
/// <summary>
6278
/// Gets or sets the scopes (required).
6379
/// </summary>

Diff for: src/OidcClient/ResponseProcessor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ private async Task<TokenResponse> RedeemCodeAsync(string code, AuthorizeState st
184184

185185
ClientId = _options.ClientId,
186186
ClientSecret = _options.ClientSecret,
187-
ClientAssertion = _options.ClientAssertion,
187+
ClientAssertion = await _options.GetClientAssertionAsync(),
188188
ClientCredentialStyle = _options.TokenClientCredentialStyle,
189189

190190
Code = code,

Diff for: test/OidcClient.Tests/ConfigurationTests.cs

+14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44

55
using FluentAssertions;
6+
using IdentityModel.Client;
67
using IdentityModel.Jwk;
78
using IdentityModel.OidcClient.Tests.Infrastructure;
89
using System;
@@ -173,5 +174,18 @@ public void Error401_while_loading_discovery_document_should_throw()
173174

174175
act.Should().Throw<InvalidOperationException>().Where(e => e.Message.Equals("Error loading discovery document: Error connecting to https://authority/.well-known/openid-configuration: not found"));
175176
}
177+
178+
[Fact]
179+
public async Task GetClientAssertionAsync_should_return_statically_configured_client_assertion_by_default()
180+
{
181+
var options = new OidcClientOptions
182+
{
183+
ClientAssertion = new ClientAssertion { Type = "test", Value = "expected" }
184+
};
185+
186+
var result = await options.GetClientAssertionAsync();
187+
result.Type.Should().Be("test");
188+
result.Value.Should().Be("expected");
189+
}
176190
}
177191
}

0 commit comments

Comments
 (0)