Skip to content

Commit 7becccc

Browse files
committed
Allow a default ITokenRequestCustomizer to be registered
This is useful to ensure that a single TokenRequestCustomizer can be used for various paths such as ITokenRetrievers as well as the HttpContextExtensions.
1 parent e2ed624 commit 7becccc

24 files changed

+1332
-433
lines changed

access-token-management/src/AccessTokenManagement.OpenIdConnect/HttpContextExtensions.cs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@ public static async Task<TokenResult<UserToken>> GetUserAccessTokenAsync(
2828
CT ct = default)
2929
{
3030
var service = httpContext.RequestServices.GetRequiredService<IUserTokenManager>();
31+
var requestParameters = parameters ?? new UserTokenRequestParameters();
32+
var tokenRequestCustomizer = httpContext.RequestServices.GetService<ITokenRequestCustomizer?>();
33+
if (tokenRequestCustomizer is not null)
34+
{
35+
var httpRequestContext = httpContext.Request.ToHttpRequestContext();
36+
requestParameters =
37+
await tokenRequestCustomizer.Customize(httpRequestContext, requestParameters, ct) as
38+
UserTokenRequestParameters;
39+
}
3140

32-
return await service.GetAccessTokenAsync(httpContext.User, parameters, ct).ConfigureAwait(false);
41+
return await service.GetAccessTokenAsync(httpContext.User, requestParameters, ct).ConfigureAwait(false);
3342
}
3443

3544
/// <summary>
@@ -45,8 +54,17 @@ public static async Task RevokeRefreshTokenAsync(
4554
CT ct = default)
4655
{
4756
var service = httpContext.RequestServices.GetRequiredService<IUserTokenManager>();
57+
var requestParameters = parameters ?? new UserTokenRequestParameters();
58+
var tokenRequestCustomizer = httpContext.RequestServices.GetService<ITokenRequestCustomizer?>();
59+
if (tokenRequestCustomizer is not null)
60+
{
61+
var httpRequestContext = httpContext.Request.ToHttpRequestContext();
62+
requestParameters =
63+
await tokenRequestCustomizer.Customize(httpRequestContext, requestParameters, ct) as
64+
UserTokenRequestParameters;
65+
}
4866

49-
await service.RevokeRefreshTokenAsync(httpContext.User, parameters, ct).ConfigureAwait(false);
67+
await service.RevokeRefreshTokenAsync(httpContext.User, requestParameters, ct).ConfigureAwait(false);
5068
}
5169

5270
/// <summary>
@@ -72,20 +90,34 @@ public static async Task<TokenResult<ClientCredentialsToken>> GetClientAccessTok
7290
var defaultScheme = await schemes.GetDefaultChallengeSchemeAsync().ConfigureAwait(false);
7391
if (defaultScheme == null)
7492
{
75-
throw new InvalidOperationException("Cannot retrieve client access token. No scheme was provided and default challenge scheme was not set.");
93+
throw new InvalidOperationException(
94+
"Cannot retrieve client access token. No scheme was provided and default challenge scheme was not set.");
7695
}
7796

7897
schemeName = Scheme.Parse(defaultScheme.Name);
7998
}
8099

100+
var requestParameters = parameters ?? new UserTokenRequestParameters();
101+
var tokenRequestCustomizer = httpContext.RequestServices.GetService<ITokenRequestCustomizer?>();
102+
if (tokenRequestCustomizer is not null)
103+
{
104+
var httpRequestContext = httpContext.Request.ToHttpRequestContext();
105+
requestParameters =
106+
await tokenRequestCustomizer.Customize(httpRequestContext, requestParameters, ct) as
107+
UserTokenRequestParameters;
108+
}
109+
81110
return await service.GetAccessTokenAsync(
82111
schemeName.Value.ToClientName(),
83-
parameters,
112+
requestParameters,
84113
ct).ConfigureAwait(false);
85114
}
86115

87116
const string AuthenticationPropertiesDPoPKey = ".Token.dpop_proof_key";
88-
internal static void SetProofKey(this AuthenticationProperties properties, DPoPProofKey dpopProofKey) => properties.Items[AuthenticationPropertiesDPoPKey] = dpopProofKey.ToString();
117+
118+
internal static void SetProofKey(this AuthenticationProperties properties, DPoPProofKey dpopProofKey) =>
119+
properties.Items[AuthenticationPropertiesDPoPKey] = dpopProofKey.ToString();
120+
89121
internal static DPoPProofKey? GetProofKey(this AuthenticationProperties properties)
90122
{
91123
if (properties.Items.TryGetValue(AuthenticationPropertiesDPoPKey, out var key))
@@ -97,17 +129,22 @@ public static async Task<TokenResult<ClientCredentialsToken>> GetClientAccessTok
97129

98130
return DPoPProofKey.Parse(key);
99131
}
132+
100133
return null;
101134
}
102135

103136
const string HttpContextDPoPKey = "dpop_proof_key";
104-
internal static void SetCodeExchangeDPoPKey(this HttpContext context, DPoPProofKey dpopProofKey) => context.Items[HttpContextDPoPKey] = dpopProofKey;
137+
138+
internal static void SetCodeExchangeDPoPKey(this HttpContext context, DPoPProofKey dpopProofKey) =>
139+
context.Items[HttpContextDPoPKey] = dpopProofKey;
140+
105141
internal static DPoPProofKey? GetCodeExchangeDPoPKey(this HttpContext context)
106142
{
107143
if (context.Items.TryGetValue(HttpContextDPoPKey, out var item))
108144
{
109145
return item as DPoPProofKey?;
110146
}
147+
111148
return null;
112149
}
113150
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Duende Software. 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.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Http.Extensions;
6+
7+
namespace Duende.AccessTokenManagement.OpenIdConnect;
8+
9+
internal static class HttpRequestContextExtensions
10+
{
11+
public static HttpRequestContext ToHttpRequestContext(this HttpRequest request) =>
12+
new()
13+
{
14+
Method = request.Method,
15+
RequestUri = new Uri(request.GetEncodedUrl()),
16+
Headers = request.Headers.Select(h => new KeyValuePair<string, IEnumerable<string>>(h.Key, h.Value))
17+
};
18+
}

access-token-management/src/AccessTokenManagement.OpenIdConnect/Internal/OpenIdConnectClientAccessTokenRetriever.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal class OpenIdConnectClientAccessTokenRetriever(
3434
};
3535

3636
var customizedParameters = customizer != null
37-
? await customizer.Customize(request, baseParameters, ct)
37+
? await customizer.Customize(request.ToHttpRequestContext(), baseParameters, ct)
3838
: baseParameters;
3939

4040
var userTokenRequestParameters = baseParameters with

access-token-management/src/AccessTokenManagement.OpenIdConnect/Internal/OpenIdConnectUserAccessTokenRetriever.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ internal class OpenIdConnectUserAccessTokenRetriever(
2929
};
3030

3131
var customizedParameters = customizer != null
32-
? await customizer.Customize(request, baseParameters, ct)
32+
? await customizer.Customize(request.ToHttpRequestContext(), baseParameters, ct)
3333
: baseParameters;
3434

3535
var parameters = baseParameters with
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace Duende.AccessTokenManagement;
5+
6+
/// <summary>
7+
/// Represents a slim version of an HTTP request
8+
/// </summary>
9+
public record struct HttpRequestContext
10+
{
11+
public required string Method { get; init; }
12+
public required Uri? RequestUri { get; init; }
13+
public required IEnumerable<KeyValuePair<string, IEnumerable<string>>> Headers { get; init; }
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace Duende.AccessTokenManagement;
5+
6+
internal static class HttpRequestContextExtensions
7+
{
8+
public static HttpRequestContext ToHttpRequestContext(this HttpRequestMessage request) =>
9+
new()
10+
{
11+
Method = request.Method.Method,
12+
RequestUri = request.RequestUri,
13+
Headers = request.Headers
14+
};
15+
}

access-token-management/src/AccessTokenManagement/ITokenRequestCustomizer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public interface ITokenRequestCustomizer
1616
/// <param name="cancellationToken">Cancellation token</param>
1717
/// <returns>Customized token request parameters</returns>
1818
Task<TokenRequestParameters> Customize(
19-
HttpRequestMessage httpRequest,
19+
HttpRequestContext httpRequest,
2020
TokenRequestParameters baseParameters,
2121
CancellationToken cancellationToken = default);
2222
}

access-token-management/src/AccessTokenManagement/Internal/ClientCredentialsTokenRetriever.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
namespace Duende.AccessTokenManagement.Internal;
77

8-
98
/// <summary>
109
/// An <see cref="AccessTokenRequestHandler.ITokenRetriever" /> implementation that retrieves a token using the client credentials flow.
1110
/// </summary>
@@ -24,7 +23,7 @@ internal class ClientCredentialsTokenRetriever(
2423
};
2524

2625
var parameters = customizer != null
27-
? await customizer.Customize(request, baseParameters, ct)
26+
? await customizer.Customize(request.ToHttpRequestContext(), baseParameters, ct)
2827
: baseParameters;
2928

3029
var getTokenResult = await clientCredentialsTokenManager.GetAccessTokenAsync(clientName, parameters, ct);

access-token-management/test/AccessTokenManagement.Tests/Framework/AppHost.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,10 @@ await context.ChallengeAsync(new AuthenticationProperties
165165
await context.Response.WriteAsJsonAsync(getResult.FailedResult);
166166
});
167167

168-
endpoints.MapGet("/call_api", async (IHttpClientFactory factory, HttpContext _) =>
168+
endpoints.MapGet("/call_api", async (IHttpClientFactory factory, HttpContext context) =>
169169
{
170170
var http = factory.CreateClient("callApi");
171-
var response = await http.GetAsync("test");
171+
var response = await http.GetAsync("test" + context.Request.QueryString);
172172
return await response.Content.ReadFromJsonAsync<TokenEchoResponse>();
173173
});
174174

access-token-management/test/AccessTokenManagement.Tests/Framework/IdentityServerHost.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public IdentityServerHost(WriteTestOutput writeTestOutput, string baseAddress =
4040
];
4141

4242
public List<Dictionary<string, string>> CapturedTokenRequests { get; } = [];
43+
public List<Dictionary<string, string>> CapturedRevocationRequests { get; } = [];
4344

4445
private void ConfigureServices(IServiceCollection services)
4546
{
@@ -80,6 +81,14 @@ private void Configure(IApplicationBuilder app)
8081
kvp => kvp.Value.ToString());
8182
CapturedTokenRequests.Add(capturedData);
8283
}
84+
else if (ctx.Request.Path == "/connect/revocation" && ctx.Request.Method == "POST")
85+
{
86+
var form = await ctx.Request.ReadFormAsync();
87+
var capturedData = form.ToDictionary(
88+
kvp => kvp.Key,
89+
kvp => kvp.Value.ToString());
90+
CapturedRevocationRequests.Add(capturedData);
91+
}
8392

8493
await next();
8594
});

0 commit comments

Comments
 (0)