-
Notifications
You must be signed in to change notification settings - Fork 45
Introduce the capability to refresh the dpop proof on a retry #344
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
Merged
+462
−113
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
...dc-client/samples/NetCoreConsoleClient/src/NetCoreConsoleClient/ClientAssertionService.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Copyright (c) Duende Software. All rights reserved. | ||
| // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. | ||
|
|
||
| using System.Security.Cryptography; | ||
| using Duende.IdentityModel; | ||
| using Duende.IdentityModel.Client; | ||
| using Microsoft.IdentityModel.JsonWebTokens; | ||
| using Microsoft.IdentityModel.Tokens; | ||
|
|
||
| namespace ConsoleClientWithBrowser; | ||
|
|
||
| /// <summary> | ||
| /// Creates signed client assertion JWTs (RFC 7523 / private_key_jwt). | ||
| /// Each call to <see cref="CreateAssertionAsync"/> produces a JWT with a fresh | ||
| /// <c>jti</c> and <c>iat</c>, which is critical when retries (e.g. DPoP nonce | ||
| /// challenges) require a new assertion to avoid replay rejection. | ||
| /// </summary> | ||
| public class ClientAssertionService | ||
| { | ||
| private readonly string _clientId; | ||
| private readonly string _audience; | ||
| private readonly SigningCredentials _signingCredentials; | ||
|
|
||
| public ClientAssertionService(string clientId, string audience, SigningCredentials signingCredentials) | ||
| { | ||
| _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); | ||
| _audience = audience ?? throw new ArgumentNullException(nameof(audience)); | ||
| _signingCredentials = signingCredentials ?? throw new ArgumentNullException(nameof(signingCredentials)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a fresh <see cref="ClientAssertion"/> with a unique <c>jti</c>. | ||
| /// </summary> | ||
| public Task<ClientAssertion> CreateAssertionAsync() | ||
| { | ||
| var now = DateTime.UtcNow; | ||
|
|
||
| var descriptor = new SecurityTokenDescriptor | ||
| { | ||
| Issuer = _clientId, | ||
| Audience = _audience, | ||
| IssuedAt = now, | ||
| NotBefore = now, | ||
| Expires = now.AddMinutes(1), | ||
| SigningCredentials = _signingCredentials, | ||
| AdditionalHeaderClaims = new Dictionary<string, object> | ||
| { | ||
| { "typ", "client-authentication+jwt" } | ||
| }, | ||
| Claims = new Dictionary<string, object> | ||
| { | ||
| { JwtClaimTypes.JwtId, Guid.NewGuid().ToString() }, | ||
| { JwtClaimTypes.Subject, _clientId }, | ||
| } | ||
| }; | ||
|
|
||
| var handler = new JsonWebTokenHandler(); | ||
| var jwt = handler.CreateToken(descriptor); | ||
|
|
||
| return Task.FromResult(new ClientAssertion | ||
| { | ||
| Type = OidcConstants.ClientAssertionTypes.JwtBearer, | ||
| Value = jwt | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a new RSA signing credential suitable for client assertion signing. | ||
| /// </summary> | ||
| public static SigningCredentials CreateSigningCredentials() | ||
| { | ||
| var rsa = RSA.Create(2048); | ||
| var key = new RsaSecurityKey(rsa); | ||
| return new SigningCredentials(key, SecurityAlgorithms.RsaSha256); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
248 changes: 144 additions & 104 deletions
248
identity-model-oidc-client/samples/NetCoreConsoleClient/src/NetCoreConsoleClient/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,134 +1,174 @@ | ||
| using Duende.IdentityModel.Client; | ||
| // Copyright (c) Duende Software. All rights reserved. | ||
| // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. | ||
|
|
||
| // | ||
| // This sample demonstrates using OidcClient with DPoP and a ClientAssertionFactory | ||
| // so that DPoP nonce retries automatically regenerate the client_assertion JWT. | ||
| // | ||
|
|
||
| using System.Security.Cryptography; | ||
| using System.Text.Json; | ||
| using Duende.IdentityModel.Client; | ||
| using Duende.IdentityModel.OidcClient; | ||
| using Newtonsoft.Json.Linq; | ||
| using Duende.IdentityModel.OidcClient.DPoP; | ||
| using Microsoft.IdentityModel.Tokens; | ||
| using Serilog; | ||
|
|
||
| namespace ConsoleClientWithBrowser | ||
| namespace ConsoleClientWithBrowser; | ||
|
|
||
| public class Program | ||
| { | ||
| public class Program | ||
| static string _authority = "https://demo.duendesoftware.com"; | ||
| static string _api = "https://demo.duendesoftware.com/api/test"; | ||
|
|
||
| static HttpClient _apiClient = new HttpClient { BaseAddress = new Uri(_api) }; | ||
|
|
||
| public static void Main(string[] args) => MainAsync().GetAwaiter().GetResult(); | ||
|
|
||
| public static async Task MainAsync() | ||
| { | ||
| static string _authority = "https://demo.duendesoftware.com"; | ||
| static string _api = "https://demo.duendesoftware.com/api/test"; | ||
| Console.WriteLine("+-----------------------------------------+"); | ||
| Console.WriteLine("| Sign in with OIDC + DPoP + Assertion |"); | ||
| Console.WriteLine("+-----------------------------------------+"); | ||
| Console.WriteLine(""); | ||
| Console.WriteLine("Press any key to sign in..."); | ||
| Console.ReadKey(); | ||
|
|
||
| await Login(); | ||
| } | ||
|
|
||
| static HttpClient _apiClient = new HttpClient { BaseAddress = new Uri(_api) }; | ||
| private static async Task Login() | ||
| { | ||
| // create a redirect URI using an available port on the loopback address. | ||
| var browser = new SystemBrowser(); | ||
| string redirectUri = string.Format($"http://127.0.0.1:{browser.Port}"); | ||
|
|
||
| public static void Main(string[] args) => MainAsync().GetAwaiter().GetResult(); | ||
| // Create a DPoP proof key | ||
| var dpopKey = CreateDPoPProofKey(); | ||
|
|
||
| public static async Task MainAsync() | ||
| { | ||
| Console.WriteLine("+-----------------------+"); | ||
| Console.WriteLine("| Sign in with OIDC |"); | ||
| Console.WriteLine("+-----------------------+"); | ||
| Console.WriteLine(""); | ||
| Console.WriteLine("Press any key to sign in..."); | ||
| Console.ReadKey(); | ||
|
|
||
| await Login(); | ||
| } | ||
| // Create a client assertion signing key and service | ||
| var signingCredentials = ClientAssertionService.CreateSigningCredentials(); | ||
|
|
||
| private static async Task Login() | ||
| var options = new OidcClientOptions | ||
| { | ||
| // 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}"); | ||
| Authority = _authority, | ||
| ClientId = "interactive.public", | ||
| RedirectUri = redirectUri, | ||
| Scope = "openid profile api", | ||
| FilterClaims = false, | ||
| Browser = browser, | ||
| }; | ||
|
|
||
| // Wire up DPoP | ||
| options.ConfigureDPoP(dpopKey); | ||
|
|
||
| // Wire up the client assertion factory. | ||
| // The factory produces a fresh JWT (with unique jti) on each invocation, | ||
| // which ensures DPoP nonce retries don't replay a stale assertion. | ||
| var assertionService = new ClientAssertionService( | ||
| clientId: options.ClientId, | ||
| audience: _authority, | ||
| signingCredentials: signingCredentials); | ||
| options.GetClientAssertionAsync = assertionService.CreateAssertionAsync; | ||
|
|
||
| var serilog = new LoggerConfiguration() | ||
| .MinimumLevel.Error() | ||
| .Enrich.FromLogContext() | ||
| .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}") | ||
| .CreateLogger(); | ||
|
|
||
| options.LoggerFactory.AddSerilog(serilog); | ||
|
|
||
| var oidcClient = new OidcClient(options); | ||
| var result = await oidcClient.LoginAsync(new LoginRequest()); | ||
|
|
||
| ShowResult(result); | ||
| await NextSteps(result, oidcClient); | ||
| } | ||
|
|
||
| var options = new OidcClientOptions | ||
| { | ||
| Authority = _authority, | ||
| ClientId = "interactive.public", | ||
| RedirectUri = redirectUri, | ||
| Scope = "openid profile api", | ||
| FilterClaims = false, | ||
| Browser = browser, | ||
| }; | ||
|
|
||
| var serilog = new LoggerConfiguration() | ||
| .MinimumLevel.Error() | ||
| .Enrich.FromLogContext() | ||
| .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}") | ||
| .CreateLogger(); | ||
|
|
||
| options.LoggerFactory.AddSerilog(serilog); | ||
|
|
||
| var oidcClient = new OidcClient(options); | ||
| var result = await oidcClient.LoginAsync(new LoginRequest()); | ||
|
|
||
| ShowResult(result); | ||
| await NextSteps(result, oidcClient); | ||
| /// <summary> | ||
| /// Creates a DPoP proof key (RSA, serialized as JWK JSON). | ||
| /// In production, persist this key so the same key is used across restarts. | ||
| /// </summary> | ||
| private static string CreateDPoPProofKey() | ||
| { | ||
| using var rsa = RSA.Create(2048); | ||
| var key = new RsaSecurityKey(rsa); | ||
| var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(key); | ||
| jwk.Alg = SecurityAlgorithms.RsaSha256; | ||
| return JsonSerializer.Serialize(jwk); | ||
Erwinvandervalk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| private static void ShowResult(LoginResult result) | ||
| { | ||
| if (result.IsError) | ||
| { | ||
| Console.WriteLine("\n\nError:\n{0}", result.Error); | ||
| return; | ||
| } | ||
|
|
||
| private static void ShowResult(LoginResult result) | ||
| Console.WriteLine("\n\nClaims:"); | ||
| foreach (var claim in result.User.Claims) | ||
| { | ||
| if (result.IsError) | ||
| { | ||
| Console.WriteLine("\n\nError:\n{0}", result.Error); | ||
| return; | ||
| } | ||
| Console.WriteLine("{0}: {1}", claim.Type, claim.Value); | ||
| } | ||
|
|
||
| Console.WriteLine("\n\nClaims:"); | ||
| foreach (var claim in result.User.Claims) | ||
| { | ||
| Console.WriteLine("{0}: {1}", claim.Type, claim.Value); | ||
| } | ||
| Console.WriteLine($"\nidentity token: {result.IdentityToken}"); | ||
| Console.WriteLine($"access token: {result.AccessToken}"); | ||
| Console.WriteLine($"refresh token: {result?.RefreshToken ?? "none"}"); | ||
| } | ||
|
|
||
| Console.WriteLine($"\nidentity token: {result.IdentityToken}"); | ||
| Console.WriteLine($"access token: {result.AccessToken}"); | ||
| Console.WriteLine($"refresh token: {result?.RefreshToken ?? "none"}"); | ||
| } | ||
| private static async Task NextSteps(LoginResult result, OidcClient oidcClient) | ||
| { | ||
| var currentAccessToken = result.AccessToken; | ||
| var currentRefreshToken = result.RefreshToken; | ||
|
|
||
| var menu = " x...exit c...call api "; | ||
| if (currentRefreshToken != null) menu += "r...refresh token "; | ||
|
|
||
| private static async Task NextSteps(LoginResult result, OidcClient oidcClient) | ||
| while (true) | ||
| { | ||
| var currentAccessToken = result.AccessToken; | ||
| var currentRefreshToken = result.RefreshToken; | ||
| Console.WriteLine("\n\n"); | ||
|
|
||
| var menu = " x...exit c...call api "; | ||
| if (currentRefreshToken != null) menu += "r...refresh token "; | ||
| Console.Write(menu); | ||
| var key = Console.ReadKey(); | ||
|
|
||
| while (true) | ||
| if (key.Key == ConsoleKey.X) return; | ||
| if (key.Key == ConsoleKey.C) await CallApi(currentAccessToken); | ||
| if (key.Key == ConsoleKey.R) | ||
| { | ||
| Console.WriteLine("\n\n"); | ||
|
|
||
| Console.Write(menu); | ||
| var key = Console.ReadKey(); | ||
|
|
||
| if (key.Key == ConsoleKey.X) return; | ||
| if (key.Key == ConsoleKey.C) await CallApi(currentAccessToken); | ||
| if (key.Key == ConsoleKey.R) | ||
| var refreshResult = await oidcClient.RefreshTokenAsync(currentRefreshToken); | ||
| if (refreshResult.IsError) | ||
| { | ||
| var refreshResult = await oidcClient.RefreshTokenAsync(currentRefreshToken); | ||
| if (refreshResult.IsError) | ||
| { | ||
| Console.WriteLine($"Error: {refreshResult.Error}"); | ||
| } | ||
| else | ||
| { | ||
| currentRefreshToken = refreshResult.RefreshToken; | ||
| currentAccessToken = refreshResult.AccessToken; | ||
|
|
||
| Console.WriteLine("\n\n"); | ||
| Console.WriteLine($"access token: {refreshResult.AccessToken}"); | ||
| Console.WriteLine($"refresh token: {refreshResult?.RefreshToken ?? "none"}"); | ||
| } | ||
| Console.WriteLine($"Error: {refreshResult.Error}"); | ||
| } | ||
| else | ||
| { | ||
| currentRefreshToken = refreshResult.RefreshToken; | ||
| currentAccessToken = refreshResult.AccessToken; | ||
|
|
||
| Console.WriteLine("\n\n"); | ||
| Console.WriteLine($"access token: {refreshResult.AccessToken}"); | ||
| Console.WriteLine($"refresh token: {refreshResult?.RefreshToken ?? "none"}"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static async Task CallApi(string currentAccessToken) | ||
| { | ||
| _apiClient.SetBearerToken(currentAccessToken); | ||
| var response = await _apiClient.GetAsync(""); | ||
| private static async Task CallApi(string currentAccessToken) | ||
| { | ||
| _apiClient.SetBearerToken(currentAccessToken); | ||
| var response = await _apiClient.GetAsync(""); | ||
|
|
||
| if (response.IsSuccessStatusCode) | ||
| { | ||
| var json = JArray.Parse(await response.Content.ReadAsStringAsync()); | ||
| Console.WriteLine("\n\n"); | ||
| Console.WriteLine(json); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"Error: {response.ReasonPhrase}"); | ||
| } | ||
| if (response.IsSuccessStatusCode) | ||
| { | ||
| var json = await response.Content.ReadAsStringAsync(); | ||
| Console.WriteLine("\n\n"); | ||
| Console.WriteLine(json); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"Error: {response.ReasonPhrase}"); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.