Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function to options to create client assertions dynamically #422

Merged
merged 1 commit into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions clients/ConsoleClientWithBrowser/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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<Claim>()
{
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,
Expand Down
2 changes: 1 addition & 1 deletion src/OidcClient/OidcClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ public virtual async Task<RefreshTokenResult> 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,
Expand Down
16 changes: 16 additions & 0 deletions src/OidcClient/OidcClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -18,6 +20,14 @@ namespace IdentityModel.OidcClient
/// </summary>
public class OidcClientOptions
{
/// <summary>
/// Creates an instance of the OidcClientOptions class.
/// </summary>
public OidcClientOptions()
{
GetClientAssertionAsync ??= () => Task.FromResult(ClientAssertion);
}

/// <summary>
/// Gets or sets the authority.
/// </summary>
Expand Down Expand Up @@ -58,6 +68,12 @@ public class OidcClientOptions
/// </value>
public ClientAssertion ClientAssertion { get; set; } = new ClientAssertion();

/// <summary>
/// Gets or sets a callback that computes the client assertion. By default, this returns the statically configured ClientAssertion
/// </summary>
[JsonIgnore]
public Func<Task<ClientAssertion>> GetClientAssertionAsync { get; set; }

/// <summary>
/// Gets or sets the scopes (required).
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/OidcClient/ResponseProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private async Task<TokenResponse> RedeemCodeAsync(string code, AuthorizeState st

ClientId = _options.ClientId,
ClientSecret = _options.ClientSecret,
ClientAssertion = _options.ClientAssertion,
ClientAssertion = await _options.GetClientAssertionAsync(),
ClientCredentialStyle = _options.TokenClientCredentialStyle,

Code = code,
Expand Down
14 changes: 14 additions & 0 deletions test/OidcClient.Tests/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


using FluentAssertions;
using IdentityModel.Client;
using IdentityModel.Jwk;
using IdentityModel.OidcClient.Tests.Infrastructure;
using System;
Expand Down Expand Up @@ -173,5 +174,18 @@ public void Error401_while_loading_discovery_document_should_throw()

act.Should().Throw<InvalidOperationException>().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");
}
}
}
Loading