Skip to content

Commit c31dbf2

Browse files
ubikAndrey Chayka
andauthored
add client assertion update callback event (DuendeArchive#122)
* add client assertion update callback event * change assertion type in tests * cleanup * Now => UtcNow * Remove static ClientAssertion configuration * Cleanup Co-authored-by: Andrey Chayka <achayka@ptsecurity.ru>
1 parent d38583b commit c31dbf2

7 files changed

Lines changed: 211 additions & 68 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Dominick Baier & Brock Allen. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using IdentityModel.Client;
6+
using Microsoft.AspNetCore.Authentication;
7+
using Microsoft.AspNetCore.Http;
8+
9+
namespace IdentityModel.AspNetCore.OAuth2Introspection
10+
{
11+
/// <summary>
12+
/// Context for the UpdateClientAssertion event
13+
/// </summary>
14+
public class UpdateClientAssertionContext : ResultContext<OAuth2IntrospectionOptions>
15+
{
16+
/// <summary>
17+
/// ctor
18+
/// </summary>
19+
public UpdateClientAssertionContext(
20+
HttpContext context,
21+
AuthenticationScheme scheme,
22+
OAuth2IntrospectionOptions options)
23+
: base(context, scheme, options) { }
24+
25+
/// <summary>
26+
/// The client assertion
27+
/// </summary>
28+
public ClientAssertion ClientAssertion { get; set; }
29+
30+
/// <summary>
31+
/// The client assertion expiration time
32+
/// </summary>
33+
public DateTime ClientAssertionExpirationTime { get; set; }
34+
}
35+
}

src/OAuth2IntrospectionEvents.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ public class OAuth2IntrospectionEvents
2121
/// </summary>
2222
public Func<TokenValidatedContext, Task> OnTokenValidated { get; set; } = context => Task.CompletedTask;
2323

24+
/// <summary>
25+
/// Invoked when client assertion need to be updated.
26+
/// </summary>
27+
public Func<UpdateClientAssertionContext, Task> OnUpdateClientAssertion { get; set; } = context => Task.CompletedTask;
28+
2429
/// <summary>
2530
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
2631
/// </summary>
@@ -30,5 +35,10 @@ public class OAuth2IntrospectionEvents
3035
/// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
3136
/// </summary>
3237
public virtual Task TokenValidated(TokenValidatedContext context) => OnTokenValidated(context);
38+
39+
/// <summary>
40+
/// Invoked when client assertion need to be updated.
41+
/// </summary>
42+
public virtual Task UpdateClientAssertion(UpdateClientAssertionContext context) => OnUpdateClientAssertion(context);
3343
}
3444
}

src/OAuth2IntrospectionHandler.cs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Dominick Baier & Brock Allen. All rights reserved.
1+
// Copyright (c) Dominick Baier & Brock Allen. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
33

44
using System;
@@ -110,7 +110,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
110110
{
111111
Lazy<Task<TokenIntrospectionResponse>> GetTokenIntrospectionResponseLazy(string _)
112112
{
113-
return new Lazy<Task<TokenIntrospectionResponse>>(async () => await LoadClaimsForToken(token, Options));
113+
return new Lazy<Task<TokenIntrospectionResponse>>(async () => await LoadClaimsForToken(token, Context, Scheme, Events, Options));
114114
}
115115

116116
var response = await IntrospectionDictionary
@@ -175,10 +175,56 @@ private static async Task<AuthenticateResult> ReportNonSuccessAndReturn(
175175
return AuthenticateResult.Fail(error);
176176
}
177177

178-
private static async Task<TokenIntrospectionResponse> LoadClaimsForToken(string token, OAuth2IntrospectionOptions options)
178+
private static async Task<TokenIntrospectionResponse> LoadClaimsForToken(
179+
string token,
180+
HttpContext context,
181+
AuthenticationScheme scheme,
182+
OAuth2IntrospectionEvents events,
183+
OAuth2IntrospectionOptions options)
179184
{
180185
var introspectionClient = await options.IntrospectionClient.Value.ConfigureAwait(false);
181-
return await introspectionClient.Introspect(token, options.TokenTypeHint).ConfigureAwait(false);
186+
using var request = CreateTokenIntrospectionRequest(token, context, scheme, events, options);
187+
return await introspectionClient.IntrospectTokenAsync(request).ConfigureAwait(false);
188+
}
189+
190+
private static TokenIntrospectionRequest CreateTokenIntrospectionRequest(
191+
string token,
192+
HttpContext context,
193+
AuthenticationScheme scheme,
194+
OAuth2IntrospectionEvents events,
195+
OAuth2IntrospectionOptions options)
196+
{
197+
if (options.ClientSecret == null && options.ClientAssertionExpirationTime <= DateTime.UtcNow)
198+
{
199+
lock (options.AssertionUpdateLockObj)
200+
{
201+
if (options.ClientAssertionExpirationTime <= DateTime.UtcNow)
202+
{
203+
var updateClientAssertionContext = new UpdateClientAssertionContext(context, scheme, options)
204+
{
205+
ClientAssertion = options.ClientAssertion ?? new ClientAssertion()
206+
};
207+
208+
events.UpdateClientAssertion(updateClientAssertionContext);
209+
210+
options.ClientAssertion = updateClientAssertionContext.ClientAssertion;
211+
options.ClientAssertionExpirationTime =
212+
updateClientAssertionContext.ClientAssertionExpirationTime;
213+
}
214+
}
215+
}
216+
217+
return new TokenIntrospectionRequest
218+
{
219+
Token = token,
220+
TokenTypeHint = options.TokenTypeHint,
221+
Address = options.IntrospectionEndpoint,
222+
ClientId = options.ClientId,
223+
ClientSecret = options.ClientSecret,
224+
ClientAssertion = options.ClientAssertion ?? new ClientAssertion(),
225+
ClientCredentialStyle = options.ClientCredentialStyle,
226+
AuthorizationHeaderStyle = options.AuthorizationHeaderStyle,
227+
};
182228
}
183229

184230
private static async Task<AuthenticateResult> CreateTicket(

src/OAuth2IntrospectionOptions.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// Copyright (c) Dominick Baier & Brock Allen. All rights reserved.
1+
// Copyright (c) Dominick Baier & Brock Allen. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
33

44
using IdentityModel.AspNetCore.OAuth2Introspection.Infrastructure;
55
using IdentityModel.Client;
66
using Microsoft.AspNetCore.Authentication;
77
using Microsoft.AspNetCore.Http;
88
using System;
9+
using System.Net.Http;
910

1011
namespace IdentityModel.AspNetCore.OAuth2Introspection
1112
{
@@ -44,10 +45,11 @@ public OAuth2IntrospectionOptions()
4445
/// </summary>
4546
public string ClientSecret { get; set; }
4647

47-
/// <summary>
48-
/// Specifies the the client assertion to be used (optional replacement of simple client secret)
49-
/// </summary>
50-
public ClientAssertion ClientAssertion { get; set; } = new ClientAssertion();
48+
internal object AssertionUpdateLockObj = new object();
49+
50+
internal ClientAssertion ClientAssertion { get; set; }
51+
52+
internal DateTime ClientAssertionExpirationTime { get; set; }
5153

5254
/// <summary>
5355
/// Specifies how client id and secret are being sent
@@ -125,7 +127,7 @@ public OAuth2IntrospectionOptions()
125127
set { base.Events = value; }
126128
}
127129

128-
internal AsyncLazy<IntrospectionClient> IntrospectionClient { get; set; }
130+
internal AsyncLazy<HttpClient> IntrospectionClient { get; set; }
129131

130132
/// <summary>
131133
/// Check that the options are valid. Should throw an exception if things are not ok.

src/PostConfigureOAuth2IntrospectionOptions.cs

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,17 @@ public void PostConfigure(string name, OAuth2IntrospectionOptions options)
3131
throw new ArgumentException("Caching is enabled, but no IDistributedCache is found in the services collection", nameof(_cache));
3232
}
3333

34-
options.IntrospectionClient = new AsyncLazy<IntrospectionClient>(() => InitializeIntrospectionClient(options));
34+
options.IntrospectionClient = new AsyncLazy<HttpClient>(() => InitializeIntrospectionClient(options));
3535
}
3636

37-
private async Task<IntrospectionClient> InitializeIntrospectionClient(OAuth2IntrospectionOptions options)
37+
private async Task<HttpClient> InitializeIntrospectionClient(OAuth2IntrospectionOptions options)
3838
{
39-
string endpoint;
40-
41-
if (options.IntrospectionEndpoint.IsPresent())
42-
{
43-
endpoint = options.IntrospectionEndpoint;
44-
}
45-
else
39+
if (!options.IntrospectionEndpoint.IsPresent())
4640
{
47-
endpoint = await GetIntrospectionEndpointFromDiscoveryDocument(options).ConfigureAwait(false);
48-
options.IntrospectionEndpoint = endpoint;
41+
options.IntrospectionEndpoint = await GetIntrospectionEndpointFromDiscoveryDocument(options).ConfigureAwait(false);
4942
}
5043

51-
HttpMessageInvoker clientFunc() => _httpClientFactory.CreateClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName);
52-
53-
return new IntrospectionClient(clientFunc, new IntrospectionClientOptions
54-
{
55-
Address = endpoint,
56-
ClientId = options.ClientId,
57-
ClientSecret = options.ClientSecret,
58-
ClientAssertion = options.ClientAssertion ?? new ClientAssertion(),
59-
ClientCredentialStyle = options.ClientCredentialStyle,
60-
AuthorizationHeaderStyle = options.AuthorizationHeaderStyle
61-
});
44+
return _httpClientFactory.CreateClient(OAuth2IntrospectionDefaults.BackChannelHttpClientName);
6245
}
6346

6447
private async Task<string> GetIntrospectionEndpointFromDiscoveryDocument(OAuth2IntrospectionOptions options)

test/Tests/Introspection.cs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ namespace Tests
2020
{
2121
public class Introspection
2222
{
23+
24+
private static readonly string clientId = "client";
25+
private static readonly string clientSecret = "secret";
26+
2327
readonly Action<OAuth2IntrospectionOptions> _options = (o) =>
2428
{
2529
o.Authority = "https://authority.com";
2630
o.DiscoveryPolicy.RequireKeySet = false;
2731

28-
o.ClientId = "scope";
29-
o.ClientSecret = "secret";
32+
o.ClientId = clientId;
33+
o.ClientSecret = clientSecret;
3034
};
3135

3236
[Fact]
@@ -46,11 +50,61 @@ public async Task ActiveToken()
4650
{
4751
var handler = new IntrospectionEndpointHandler(IntrospectionEndpointHandler.Behavior.Active);
4852

49-
var client = PipelineFactory.CreateClient((o) => _options(o), handler);
53+
var client = PipelineFactory.CreateClient(_options, handler);
54+
client.SetBearerToken("sometoken");
55+
56+
var result = await client.GetAsync("http://test");
57+
result.StatusCode.Should().Be(HttpStatusCode.OK);
58+
59+
var request = handler.LastRequest;
60+
request.Should().ContainKey("client_id").WhichValue.Should().Be(clientId);
61+
request.Should().ContainKey("client_secret").WhichValue.Should().Be(clientSecret);
62+
}
63+
64+
[Theory]
65+
[InlineData(5000, "testAssertion1", "testAssertion1")]
66+
[InlineData(-5000, "testAssertion1", "testAssertion2")]
67+
public async Task ActiveToken_With_ClientAssertion(int ttl, string assertion1, string assertion2)
68+
{
69+
var handler = new IntrospectionEndpointHandler(IntrospectionEndpointHandler.Behavior.Active);
70+
var count = 0;
71+
72+
var client = PipelineFactory.CreateClient((o) =>
73+
{
74+
_options(o);
75+
o.ClientSecret = null;
76+
77+
o.Events.OnUpdateClientAssertion = e =>
78+
{
79+
count++;
80+
e.ClientAssertion = new ClientAssertion
81+
{
82+
Type = "testType",
83+
Value = "testAssertion" + count
84+
};
85+
e.ClientAssertionExpirationTime = DateTime.UtcNow.AddMilliseconds(ttl);
86+
87+
return Task.CompletedTask;
88+
};
89+
}, handler);
90+
5091
client.SetBearerToken("sometoken");
5192

5293
var result = await client.GetAsync("http://test");
5394
result.StatusCode.Should().Be(HttpStatusCode.OK);
95+
96+
var request = handler.LastRequest;
97+
request.Should().ContainKey("client_id").WhichValue.Should().Be(clientId);
98+
request.Should().ContainKey("client_assertion_type").WhichValue.Should().Be("testType");
99+
request.Should().ContainKey("client_assertion").WhichValue.Should().Be(assertion1);
100+
101+
result = await client.GetAsync("http://test");
102+
result.StatusCode.Should().Be(HttpStatusCode.OK);
103+
104+
request = handler.LastRequest;
105+
request.Should().ContainKey("client_id").WhichValue.Should().Be(clientId);
106+
request.Should().ContainKey("client_assertion_type").WhichValue.Should().Be("testType");
107+
request.Should().ContainKey("client_assertion").WhichValue.Should().Be(assertion2);
54108
}
55109

56110
[Fact]

0 commit comments

Comments
 (0)