Skip to content
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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Duende.IdentityModel" Version="8.0.1" />
<PackageVersion Include="Duende.IdentityModel" Version="8.1.0" />
<PackageVersion Include="Duende.IdentityServer" Version="7.4.2" />
<PackageVersion Include="MartinCostello.Logging.XUnit.v3" Version="0.7.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(FrameworkVersion)" />
Expand Down
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>NetCoreConsoleClient</AssemblyName>
<OutputType>Exe</OutputType>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"></FrameworkReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.15.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Duende.IdentityModel.OidcClient" Version="6.0.0" />
<ProjectReference Include="../../../../src/IdentityModel.OidcClient.Extensions/IdentityModel.OidcClient.Extensions.csproj" />
</ItemGroup>

</Project>
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);
}

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}");
}
}
}
Loading
Loading