Skip to content
This repository was archived by the owner on Nov 19, 2024. It is now read-only.

Commit 99b8520

Browse files
Merge pull request #30 from DuendeSoftware/brock/dpop-worker-host
DPoP fixes
2 parents b156257 + 08e5d58 commit 99b8520

10 files changed

+130
-22
lines changed

Diff for: samples/Web/Startup.cs

+13-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
33

44
using System;
5+
using System.Security.Cryptography;
6+
using System.Text.Json;
57
using Microsoft.AspNetCore.Authentication;
68
using Microsoft.AspNetCore.Builder;
79
using Microsoft.Extensions.DependencyInjection;
@@ -56,15 +58,24 @@ internal static WebApplication ConfigureServices(this WebApplicationBuilder buil
5658
};
5759
});
5860

61+
var rsaKey = new RsaSecurityKey(RSA.Create(2048));
62+
var jsonWebKey = JsonWebKeyConverter.ConvertFromRSASecurityKey(rsaKey);
63+
jsonWebKey.Alg = "PS256";
64+
var jwk = JsonSerializer.Serialize(jsonWebKey);
65+
5966
builder.Services.AddOpenIdConnectAccessTokenManagement(options =>
6067
{
61-
//const string jwk = "{\"Alg\":\"RS256\",\"Crv\":null,\"D\":\"QeBWodq0hSYjfAxxo0VZleXLqwwZZeNWvvFfES4WyItao_-OJv1wKA7zfkZxbWkpK5iRbKrl2AMJ52AtUo5JJ6QZ7IjAQlgM0lBg3ltjb1aA0gBsK5XbiXcsV8DiAnRuy6-XgjAKPR8Lo-wZl_fdPbVoAmpSdmfn_6QXXPBai5i7FiyDbQa16pI6DL-5SCj7F78QDTRiJOqn5ElNvtoJEfJBm13giRdqeriFi3pCWo7H3QBgTEWtDNk509z4w4t64B2HTXnM0xj9zLnS42l7YplJC7MRibD4nVBMtzfwtGRKLj8beuDgtW9pDlQqf7RVWX5pHQgiHAZmUi85TEbYdQ\",\"DP\":\"h2F54OMaC9qq1yqR2b55QNNaChyGtvmTHSdqZJ8lJFqvUorlz-Uocj2BTowWQnaMd8zRKMdKlSeUuSv4Z6WmjSxSsNbonI6_II5XlZLWYqFdmqDS-xCmJY32voT5Wn7OwB9xj1msDqrFPg-PqSBOh5OppjCqXqDFcNvSkQSajXc\",\"DQ\":\"VABdS20Nxkmq6JWLQj7OjRxVJuYsHrfmWJmDA7_SYtlXaPUcg-GiHGQtzdDWEeEi0dlJjv9I3FdjKGC7CGwqtVygW38DzVYJsV2EmRNJc1-j-1dRs_pK9GWR4NYm0mVz_IhS8etIf9cfRJk90xU3AL3_J6p5WNF7I5ctkLpnt8M\",\"E\":\"AQAB\",\"K\":null,\"KeyOps\":[],\"Kty\":\"RSA\",\"N\":\"yWWAOSV3Z_BW9rJEFvbZyeU-q2mJWC0l8WiHNqwVVf7qXYgm9hJC0j1aPHku_Wpl38DpK3Xu3LjWOFG9OrCqga5Pzce3DDJKI903GNqz5wphJFqweoBFKOjj1wegymvySsLoPqqDNVYTKp4nVnECZS4axZJoNt2l1S1bC8JryaNze2stjW60QT-mIAGq9konKKN3URQ12dr478m0Oh-4WWOiY4HrXoSOklFmzK-aQx1JV_SZ04eIGfSw1pZZyqTaB1BwBotiy-QA03IRxwIXQ7BSx5EaxC5uMCMbzmbvJqjt-q8Y1wyl-UQjRucgp7hkfHSE1QT3zEex2Q3NFux7SQ\",\"Oth\":null,\"P\":\"_T7MTkeOh5QyqlYCtLQ2RWf2dAJ9i3wrCx4nEDm1c1biijhtVTL7uJTLxwQIM9O2PvOi5Dq-UiGy6rhHZqf5akWTeHtaNyI-2XslQfaS3ctRgmGtRQL_VihK-R9AQtDx4eWL4h-bDJxPaxby_cVo_j2MX5AeoC1kNmcCdDf_X0M\",\"Q\":\"y5ZSThaGLjaPj8Mk2nuD8TiC-sb4aAZVh9K-W4kwaWKfDNoPcNb_dephBNMnOp9M1br6rDbyG7P-Sy_LOOsKg3Q0wHqv4hnzGaOQFeMJH4HkXYdENC7B5JG9PefbC6zwcgZWiBnsxgKpScNWuzGF8x2CC-MdsQ1bkQeTPbJklIM\",\"QI\":\"i716Vt9II_Rt6qnjsEhfE4bej52QFG9a1hSnx5PDNvRrNqR_RpTA0lO9qeXSZYGHTW_b6ZXdh_0EUwRDEDHmaxjkIcTADq6JLuDltOhZuhLUSc5NCKLAVCZlPcaSzv8-bZm57mVcIpx0KyFHxvk50___Jgx1qyzwLX03mPGUbDQ\",\"Use\":null,\"X\":null,\"X5c\":[],\"X5t\":null,\"X5tS256\":null,\"X5u\":null,\"Y\":null,\"KeySize\":2048,\"HasPrivateKey\":true,\"CryptoProviderFactory\":{\"CryptoProviderCache\":{},\"CustomCryptoProvider\":null,\"CacheSignatureProviders\":true,\"SignatureProviderObjectPoolCacheSize\":80}}";
68+
// if you uncomment this line, then be sure to change the URL for the "user_client"
69+
// to include "dpop/" at the end, since that's the DPoP enabled API path
6270
//options.DPoPJsonWebKey = jwk;
6371
});
6472

6573
// registers HTTP client that uses the managed user access token
6674
builder.Services.AddUserAccessTokenHttpClient("user_client",
67-
configureClient: client => { client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/"); });
75+
configureClient: client => {
76+
client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/");
77+
//client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/dpop/");
78+
});
6879

6980
// registers HTTP client that uses the managed client access token
7081
builder.Services.AddClientAccessTokenHttpClient("client",

Diff for: samples/Worker/Program.cs

+36-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using System;
55
using Duende.AccessTokenManagement;
66
using Serilog.Sinks.SystemConsole.Themes;
7+
using Microsoft.IdentityModel.Tokens;
8+
using System.Security.Cryptography;
9+
using System.Text.Json;
710

811
namespace WorkerService;
912

@@ -38,34 +41,62 @@ public static IHostBuilder CreateHostBuilder(string[] args)
3841

3942
client.Scope = "api";
4043
})
44+
.AddClient("demo.dpop", client =>
45+
{
46+
client.TokenEndpoint = "https://demo.duendesoftware.com/connect/token";
47+
//client.TokenEndpoint = "https://localhost:5001/connect/token";
48+
49+
client.ClientId = "m2m.dpop";
50+
//client.ClientId = "m2m.dpop.nonce";
51+
client.ClientSecret = "secret";
52+
53+
client.Scope = "api";
54+
client.DPoPJsonWebKey = CreateDPoPKey();
55+
})
4156
.AddClient("demo.jwt", client =>
4257
{
4358
client.TokenEndpoint = "https://demo.duendesoftware.com/connect/token";
4459
client.ClientId = "m2m.short.jwt";
4560

4661
client.Scope = "api";
4762
});
48-
63+
4964
services.AddClientCredentialsHttpClient("client", "demo", client =>
5065
{
5166
client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/");
5267
});
53-
68+
69+
services.AddClientCredentialsHttpClient("client.dpop", "demo.dpop", client =>
70+
{
71+
//client.BaseAddress = new Uri("https://localhost:5001/api/dpop/");
72+
client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/dpop/");
73+
});
74+
5475
services.AddHttpClient<TypedClient>(client =>
5576
{
5677
client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/");
5778
})
5879
.AddClientCredentialsTokenHandler("demo");
5980

6081
services.AddTransient<IClientAssertionService, ClientAssertionService>();
61-
82+
6283
//services.AddHostedService<WorkerManual>();
63-
services.AddHostedService<WorkerManualJwt>();
84+
//services.AddHostedService<WorkerManualJwt>();
6485
//services.AddHostedService<WorkerHttpClient>();
6586
//services.AddHostedService<WorkerTypedHttpClient>();
87+
services.AddHostedService<WorkerDPoPHttpClient>();
6688
});
6789

6890
return host;
6991
}
70-
92+
93+
private static string CreateDPoPKey()
94+
{
95+
var key = new RsaSecurityKey(RSA.Create(2048));
96+
var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(key);
97+
jwk.Alg = "PS256";
98+
var jwkJson = JsonSerializer.Serialize(jwk);
99+
return jwkJson;
100+
}
101+
71102
}

Diff for: samples/Worker/WorkerDPoPHttpClient.cs

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
using System;
7+
using System.Net.Http;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace WorkerService;
12+
13+
public class WorkerDPoPHttpClient : BackgroundService
14+
{
15+
private readonly ILogger<WorkerDPoPHttpClient> _logger;
16+
private readonly IHttpClientFactory _clientFactory;
17+
18+
public WorkerDPoPHttpClient(ILogger<WorkerDPoPHttpClient> logger, IHttpClientFactory factory)
19+
{
20+
_logger = logger;
21+
_clientFactory = factory;
22+
}
23+
24+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
25+
{
26+
await Task.Delay(2000, stoppingToken);
27+
28+
while (!stoppingToken.IsCancellationRequested)
29+
{
30+
Console.WriteLine("\n\n");
31+
_logger.LogInformation("WorkerHttpClient running at: {time}", DateTimeOffset.Now);
32+
33+
var client = _clientFactory.CreateClient("client.dpop");
34+
var response = await client.GetAsync("test?x=1", stoppingToken);
35+
36+
if (response.IsSuccessStatusCode)
37+
{
38+
var content = await response.Content.ReadAsStringAsync(stoppingToken);
39+
_logger.LogInformation("API response: {response}", content);
40+
}
41+
else
42+
{
43+
_logger.LogError("API returned: {statusCode}", response.StatusCode);
44+
}
45+
46+
await Task.Delay(5000, stoppingToken);
47+
}
48+
}
49+
}

Diff for: src/Duende.AccessTokenManagement.OpenIdConnect/DPoPProofTokenHandler.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
4343
var dPoPNonce = response.GetDPoPNonce();
4444

4545
// retry if 401
46-
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && response.IsDPoPNonceError())
46+
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && response.IsDPoPError())
4747
{
4848
response.Dispose();
4949

@@ -54,7 +54,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
5454
{
5555
await _dPoPNonceStore.StoreNonceAsync(new DPoPNonceContext
5656
{
57-
Url = request.RequestUri!.AbsoluteUri,
57+
Url = request.GetDPoPUrl(),
5858
Method = request.Method.ToString(),
5959
}, dPoPNonce);
6060
}
@@ -77,7 +77,7 @@ protected virtual async Task SetDPoPProofTokenAsync(HttpRequestMessage request,
7777
// create proof
7878
var proofToken = await _dPoPProofService.CreateProofTokenAsync(new DPoPProofRequest
7979
{
80-
Url = request.RequestUri!.AbsoluteUri,
80+
Url = request.GetDPoPUrl(),
8181
Method = request.Method.ToString(),
8282
DPoPJsonWebKey = jwk,
8383
DPoPNonce = dpopNonce,

Diff for: src/Duende.AccessTokenManagement.OpenIdConnect/UserTokenEndpointService.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,10 @@ public async Task<UserToken> RefreshAccessTokenAsync(
105105
_logger.LogDebug("refresh token request to: {endpoint}", request.Address);
106106
var response = await oidc.HttpClient!.RequestRefreshTokenAsync(request, cancellationToken).ConfigureAwait(false);
107107

108-
if (response.IsError && response.Error == OidcConstants.TokenErrors.UseDPoPNonce && dPoPJsonWebKey != null && response.DPoPNonce != null)
108+
if (response.IsError &&
109+
(response.Error == OidcConstants.TokenErrors.UseDPoPNonce || response.Error == OidcConstants.TokenErrors.InvalidDPoPProof) &&
110+
dPoPJsonWebKey != null &&
111+
response.DPoPNonce != null)
109112
{
110113
var proof = await _dPoPProofService.CreateProofTokenAsync(new DPoPProofRequest
111114
{

Diff for: src/Duende.AccessTokenManagement/AccessTokenHandler.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
5757
response.Dispose();
5858

5959
// if it's a DPoP nonce error, we don't need to obtain a new access token
60-
var force = !response.IsDPoPNonceError();
60+
var force = !response.IsDPoPError();
6161
if (!force && !string.IsNullOrEmpty(dPoPNonce))
6262
{
6363
_logger.LogDebug("DPoP nonce error invoking endpoint: {url}, retrying using new nonce", request.RequestUri?.AbsoluteUri.ToString());
@@ -70,7 +70,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
7070
{
7171
await _dPoPNonceStore.StoreNonceAsync(new DPoPNonceContext
7272
{
73-
Url = request.RequestUri!.AbsoluteUri,
73+
Url = request.GetDPoPUrl(),
7474
Method = request.Method.ToString(),
7575
}, dPoPNonce);
7676
}
@@ -122,7 +122,7 @@ protected virtual async Task<bool> SetDPoPProofTokenAsync(HttpRequestMessage req
122122
var proofToken = await _dPoPProofService.CreateProofTokenAsync(new DPoPProofRequest
123123
{
124124
AccessToken = token.AccessToken,
125-
Url = request.RequestUri!.AbsoluteUri,
125+
Url = request.GetDPoPUrl(),
126126
Method = request.Method.ToString(),
127127
DPoPJsonWebKey = token.DPoPJsonWebKey,
128128
DPoPNonce = dpopNonce,

Diff for: src/Duende.AccessTokenManagement/ClientCredentialsTokenEndpointService.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ public virtual async Task<ClientCredentialsToken> RequestToken(
145145
_logger.LogDebug("Requesting client credentials access token at endpoint: {endpoint}", request.Address);
146146
var response = await httpClient.RequestClientCredentialsTokenAsync(request, cancellationToken).ConfigureAwait(false);
147147

148-
if (response.IsError && response.Error == OidcConstants.TokenErrors.UseDPoPNonce && key != null && response.DPoPNonce != null)
148+
if (response.IsError &&
149+
(response.Error == OidcConstants.TokenErrors.UseDPoPNonce || response.Error == OidcConstants.TokenErrors.InvalidDPoPProof) &&
150+
key != null &&
151+
response.DPoPNonce != null)
149152
{
150153
_logger.LogDebug("Token request failed with DPoP nonce error. Retrying with new nonce.");
151154

Diff for: src/Duende.AccessTokenManagement/DPoPExtensions.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ public static void SetDPoPProofToken(this HttpRequestMessage request, string? pr
4343
}
4444

4545
/// <summary>
46-
/// Reads the WWW-Authenticate response header to determine if the respone is in error due to an invalid DPoP nonce
46+
/// Reads the WWW-Authenticate response header to determine if the respone is in error due to DPoP
4747
/// </summary>
48-
public static bool IsDPoPNonceError(this HttpResponseMessage response)
48+
public static bool IsDPoPError(this HttpResponseMessage response)
4949
{
5050
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
5151
{
@@ -64,10 +64,21 @@ public static bool IsDPoPNonceError(this HttpResponseMessage response)
6464
return null;
6565
}).Where(x => x != null).FirstOrDefault();
6666

67-
return error == OidcConstants.TokenErrors.UseDPoPNonce;
67+
return error == OidcConstants.TokenErrors.UseDPoPNonce || error == OidcConstants.TokenErrors.InvalidDPoPProof;
6868
}
6969
}
7070

7171
return false;
7272
}
73+
74+
/// <summary>
75+
/// Returns the URL without any query params
76+
/// </summary>
77+
/// <param name="request"></param>
78+
/// <returns></returns>
79+
public static string GetDPoPUrl(this HttpRequestMessage request)
80+
{
81+
return request.RequestUri!.Scheme + "://" + request.RequestUri!.Authority + request.RequestUri!.LocalPath;
82+
return request.RequestUri!.Scheme + "://" + request.RequestUri!.Authority + request.RequestUri!.AbsolutePath;
83+
}
7384
}

Diff for: src/Duende.AccessTokenManagement/DistributedClientCredentialsTokenCache.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public async Task SetAsync(
5353
AbsoluteExpiration = cacheExpiration
5454
};
5555

56-
_logger.LogDebug("Caching access token for client: {clientName}. Expiration: {expiration}", clientName, cacheExpiration);
56+
_logger.LogTrace("Caching access token for client: {clientName}. Expiration: {expiration}", clientName, cacheExpiration);
5757

5858
var cacheKey = GenerateCacheKey(_options, clientName, requestParameters);
5959
await _cache.SetStringAsync(cacheKey, data, entryOptions, token: cancellationToken).ConfigureAwait(false);
@@ -84,7 +84,7 @@ public async Task SetAsync(
8484
}
8585
}
8686

87-
_logger.LogDebug("Cache miss for access token for client: {clientName}", clientName);
87+
_logger.LogTrace("Cache miss for access token for client: {clientName}", clientName);
8888
return null;
8989
}
9090

Diff for: src/Duende.AccessTokenManagement/DistributedDPoPNonceStore.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public DistributedDPoPNonceStore(
4747
return entry;
4848
}
4949

50-
_logger.LogDebug("Cache miss for DPoP nonce for URL: {url}, method: {method}", context.Url, context.Method);
50+
_logger.LogTrace("Cache miss for DPoP nonce for URL: {url}, method: {method}", context.Url, context.Method);
5151
return null;
5252
}
5353

@@ -64,7 +64,7 @@ public virtual async Task StoreNonceAsync(DPoPNonceContext context, string nonce
6464
AbsoluteExpiration = cacheExpiration
6565
};
6666

67-
_logger.LogDebug("Caching DPoP nonce for URL: {url}, method: {method}. Expiration: {expiration}", context.Url, context.Method, cacheExpiration);
67+
_logger.LogTrace("Caching DPoP nonce for URL: {url}, method: {method}. Expiration: {expiration}", context.Url, context.Method, cacheExpiration);
6868

6969
var cacheKey = GenerateCacheKey(context);
7070
await _cache.SetStringAsync(cacheKey, data, entryOptions, token: cancellationToken).ConfigureAwait(false);

0 commit comments

Comments
 (0)