From b1605d989c9407679173eb89fac8638a08760961 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Tue, 2 Apr 2024 14:01:04 -0500 Subject: [PATCH] Add function to options to create client assertions dynamically --- clients/ConsoleClientWithBrowser/Program.cs | 56 +++++++++++++++++++-- src/OidcClient/OidcClient.cs | 2 +- src/OidcClient/OidcClientOptions.cs | 16 ++++++ src/OidcClient/ResponseProcessor.cs | 2 +- test/OidcClient.Tests/ConfigurationTests.cs | 14 ++++++ 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/clients/ConsoleClientWithBrowser/Program.cs b/clients/ConsoleClientWithBrowser/Program.cs index b93751f..3ba5350 100644 --- a/clients/ConsoleClientWithBrowser/Program.cs +++ b/clients/ConsoleClientWithBrowser/Program.cs @@ -7,6 +7,11 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Serilog.Sinks.SystemConsole.Themes; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using IdentityModel; +using Microsoft.IdentityModel.Tokens; +using IdentityModel.Client; namespace ConsoleClientWithBrowser { @@ -29,17 +34,62 @@ public static async Task Main() await SignIn(); } + private static string rsaKey = + "{" + + "\"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\"" + + "}"; + + private static string CreateClientToken(SigningCredentials credential, string clientId, string audience) + { + var now = DateTime.UtcNow; + + var token = new JwtSecurityToken( + clientId, + audience, + new List() + { + new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()), + new Claim(JwtClaimTypes.Subject, clientId), + new Claim(JwtClaimTypes.IssuedAt, now.ToEpochTime().ToString(), ClaimValueTypes.Integer64) + }, + now, + now.AddMinutes(1), + credential + ); + + var tokenHandler = new JwtSecurityTokenHandler(); + return tokenHandler.WriteToken(token); + } + private static async Task SignIn() { // create a redirect URI using an available port on the loopback address. // requires the OP to allow random ports on 127.0.0.1 - otherwise set a static port var browser = new SystemBrowser(); - string redirectUri = string.Format($"http://127.0.0.1:{browser.Port}"); + var redirectUri = string.Format($"http://127.0.0.1:{browser.Port}"); + var authority = "https://demo.duendesoftware.com"; + + var jwk = new JsonWebKey(rsaKey); + var credential = new SigningCredentials(jwk, "RS256"); var options = new OidcClientOptions { - Authority = "https://demo.duendesoftware.com", - ClientId = "interactive.public.short", + Authority = authority, + ClientId = "interactive.confidential.short.jwt", + GetClientAssertionAsync = () => Task.FromResult(new ClientAssertion + { + Type = OidcConstants.ClientAssertionTypes.JwtBearer, + Value = CreateClientToken(credential, "interactive.confidential.short.jwt", authority) + }), RedirectUri = redirectUri, Scope = "openid profile api offline_access", FilterClaims = false, diff --git a/src/OidcClient/OidcClient.cs b/src/OidcClient/OidcClient.cs index 20b4f33..afb4544 100644 --- a/src/OidcClient/OidcClient.cs +++ b/src/OidcClient/OidcClient.cs @@ -336,7 +336,7 @@ public virtual async Task RefreshTokenAsync( Address = Options.ProviderInformation.TokenEndpoint, ClientId = Options.ClientId, ClientSecret = Options.ClientSecret, - ClientAssertion = Options.ClientAssertion, + ClientAssertion = await Options.GetClientAssertionAsync(), ClientCredentialStyle = Options.TokenClientCredentialStyle, RefreshToken = refreshToken, Parameters = backChannelParameters, diff --git a/src/OidcClient/OidcClientOptions.cs b/src/OidcClient/OidcClientOptions.cs index c488428..6d099c2 100644 --- a/src/OidcClient/OidcClientOptions.cs +++ b/src/OidcClient/OidcClientOptions.cs @@ -10,6 +10,8 @@ using System.Net.Http; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; namespace IdentityModel.OidcClient { @@ -18,6 +20,14 @@ namespace IdentityModel.OidcClient /// public class OidcClientOptions { + /// + /// Creates an instance of the OidcClientOptions class. + /// + public OidcClientOptions() + { + GetClientAssertionAsync ??= () => Task.FromResult(ClientAssertion); + } + /// /// Gets or sets the authority. /// @@ -58,6 +68,12 @@ public class OidcClientOptions /// public ClientAssertion ClientAssertion { get; set; } = new ClientAssertion(); + /// + /// Gets or sets a callback that computes the client assertion. By default, this returns the statically configured ClientAssertion + /// + [JsonIgnore] + public Func> GetClientAssertionAsync { get; set; } + /// /// Gets or sets the scopes (required). /// diff --git a/src/OidcClient/ResponseProcessor.cs b/src/OidcClient/ResponseProcessor.cs index 4cb7fa9..513ff04 100644 --- a/src/OidcClient/ResponseProcessor.cs +++ b/src/OidcClient/ResponseProcessor.cs @@ -184,7 +184,7 @@ private async Task RedeemCodeAsync(string code, AuthorizeState st ClientId = _options.ClientId, ClientSecret = _options.ClientSecret, - ClientAssertion = _options.ClientAssertion, + ClientAssertion = await _options.GetClientAssertionAsync(), ClientCredentialStyle = _options.TokenClientCredentialStyle, Code = code, diff --git a/test/OidcClient.Tests/ConfigurationTests.cs b/test/OidcClient.Tests/ConfigurationTests.cs index 280cb9e..65fca3d 100644 --- a/test/OidcClient.Tests/ConfigurationTests.cs +++ b/test/OidcClient.Tests/ConfigurationTests.cs @@ -3,6 +3,7 @@ using FluentAssertions; +using IdentityModel.Client; using IdentityModel.Jwk; using IdentityModel.OidcClient.Tests.Infrastructure; using System; @@ -173,5 +174,18 @@ public void Error401_while_loading_discovery_document_should_throw() act.Should().Throw().Where(e => e.Message.Equals("Error loading discovery document: Error connecting to https://authority/.well-known/openid-configuration: not found")); } + + [Fact] + public async Task GetClientAssertionAsync_should_return_statically_configured_client_assertion_by_default() + { + var options = new OidcClientOptions + { + ClientAssertion = new ClientAssertion { Type = "test", Value = "expected" } + }; + + var result = await options.GetClientAssertionAsync(); + result.Type.Should().Be("test"); + result.Value.Should().Be("expected"); + } } } \ No newline at end of file