Skip to content

Commit d38583b

Browse files
authored
fix concurrency issue in OAuth2IntrospectionHandler (DuendeArchive#129)
1 parent 5053d30 commit d38583b

1 file changed

Lines changed: 64 additions & 57 deletions

File tree

src/OAuth2IntrospectionHandler.cs

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
using System.Security.Claims;
99
using System.Text.Encodings.Web;
1010
using System.Threading.Tasks;
11-
using IdentityModel.AspNetCore.OAuth2Introspection.Infrastructure;
1211
using IdentityModel.Client;
1312
using Microsoft.AspNetCore.Authentication;
13+
using Microsoft.AspNetCore.Http;
1414
using Microsoft.Extensions.Caching.Distributed;
1515
using Microsoft.Extensions.Logging;
1616
using Microsoft.Extensions.Options;
@@ -25,8 +25,8 @@ public class OAuth2IntrospectionHandler : AuthenticationHandler<OAuth2Introspect
2525
private readonly IDistributedCache _cache;
2626
private readonly ILogger<OAuth2IntrospectionHandler> _logger;
2727

28-
static readonly ConcurrentDictionary<string, Lazy<Task<AuthenticateResult>>> IntrospectionDictionary =
29-
new ConcurrentDictionary<string, Lazy<Task<AuthenticateResult>>>();
28+
static readonly ConcurrentDictionary<string, Lazy<Task<TokenIntrospectionResponse>>> IntrospectionDictionary =
29+
new ConcurrentDictionary<string, Lazy<Task<TokenIntrospectionResponse>>>();
3030

3131
/// <summary>
3232
/// Initializes a new instance of the <see cref="OAuth2IntrospectionHandler"/> class.
@@ -94,10 +94,10 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
9494
var isInActive = claims.FirstOrDefault(c => string.Equals(c.Type, "active", StringComparison.OrdinalIgnoreCase) && string.Equals(c.Value, "false", StringComparison.OrdinalIgnoreCase));
9595
if (isInActive != null)
9696
{
97-
return await ReportNonSuccessAndReturn("Cached token is not active.");
97+
return await ReportNonSuccessAndReturn("Cached token is not active.", Context, Scheme, Events, Options);
9898
}
9999

100-
return await CreateTicket(claims, token);
100+
return await CreateTicket(claims, token, Context, Scheme, Events, Options);
101101
}
102102

103103
_logger.LogTrace("Token is not cached.");
@@ -108,58 +108,64 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
108108
// with the same token come in at the same time
109109
try
110110
{
111-
return await IntrospectionDictionary.GetOrAdd(token, _ =>
111+
Lazy<Task<TokenIntrospectionResponse>> GetTokenIntrospectionResponseLazy(string _)
112112
{
113-
return new Lazy<Task<AuthenticateResult>>(async () =>
113+
return new Lazy<Task<TokenIntrospectionResponse>>(async () => await LoadClaimsForToken(token, Options));
114+
}
115+
116+
var response = await IntrospectionDictionary
117+
.GetOrAdd(token, GetTokenIntrospectionResponseLazy)
118+
.Value;
119+
120+
if (response.IsError)
121+
{
122+
_logger.LogError("Error returned from introspection endpoint: " + response.Error);
123+
return await ReportNonSuccessAndReturn("Error returned from introspection endpoint: " + response.Error, Context, Scheme, Events, Options);
124+
}
125+
126+
if (response.IsActive)
127+
{
128+
if (Options.EnableCaching)
129+
{
130+
await _cache.SetClaimsAsync(Options.CacheKeyPrefix, token, response.Claims, Options.CacheDuration, _logger).ConfigureAwait(false);
131+
}
132+
133+
return await CreateTicket(response.Claims, token, Context, Scheme, Events, Options);
134+
}
135+
else
136+
{
137+
if (Options.EnableCaching)
114138
{
115-
var response = await LoadClaimsForToken(token);
116-
117-
if (response.IsError)
118-
{
119-
_logger.LogError("Error returned from introspection endpoint: " + response.Error);
120-
return await ReportNonSuccessAndReturn("Error returned from introspection endpoint: " + response.Error);
121-
}
122-
123-
if (response.IsActive)
124-
{
125-
if (Options.EnableCaching)
126-
{
127-
await _cache.SetClaimsAsync(Options.CacheKeyPrefix, token, response.Claims, Options.CacheDuration, _logger).ConfigureAwait(false);
128-
}
129-
130-
return await CreateTicket(response.Claims, token);
131-
}
132-
else
133-
{
134-
if (Options.EnableCaching)
135-
{
136-
// add an exp claim - otherwise caching will not work
137-
var claimsWithExp = response.Claims.ToList();
138-
claimsWithExp.Add(new Claim("exp",
139-
DateTimeOffset.UtcNow.Add(Options.CacheDuration).ToUnixTimeSeconds().ToString()));
140-
await _cache.SetClaimsAsync(Options.CacheKeyPrefix, token, claimsWithExp, Options.CacheDuration, _logger)
141-
.ConfigureAwait(false);
142-
}
143-
144-
return await ReportNonSuccessAndReturn("Token is not active.");
145-
}
146-
});
147-
}).Value;
139+
// add an exp claim - otherwise caching will not work
140+
var claimsWithExp = response.Claims.ToList();
141+
claimsWithExp.Add(new Claim("exp",
142+
DateTimeOffset.UtcNow.Add(Options.CacheDuration).ToUnixTimeSeconds().ToString()));
143+
await _cache.SetClaimsAsync(Options.CacheKeyPrefix, token, claimsWithExp, Options.CacheDuration, _logger)
144+
.ConfigureAwait(false);
145+
}
146+
147+
return await ReportNonSuccessAndReturn("Token is not active.", Context, Scheme, Events, Options);
148+
}
148149
}
149150
finally
150151
{
151152
IntrospectionDictionary.TryRemove(token, out _);
152153
}
153154
}
154155

155-
private async Task<AuthenticateResult> ReportNonSuccessAndReturn(string error)
156+
private static async Task<AuthenticateResult> ReportNonSuccessAndReturn(
157+
string error,
158+
HttpContext httpContext,
159+
AuthenticationScheme scheme,
160+
OAuth2IntrospectionEvents events,
161+
OAuth2IntrospectionOptions options)
156162
{
157-
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
163+
var authenticationFailedContext = new AuthenticationFailedContext(httpContext, scheme, options)
158164
{
159165
Error = error
160166
};
161167

162-
await Events.AuthenticationFailed(authenticationFailedContext);
168+
await events.AuthenticationFailed(authenticationFailedContext);
163169

164170
if (authenticationFailedContext.Result != null)
165171
{
@@ -169,36 +175,37 @@ private async Task<AuthenticateResult> ReportNonSuccessAndReturn(string error)
169175
return AuthenticateResult.Fail(error);
170176
}
171177

172-
private AsyncLazy<TokenIntrospectionResponse> CreateLazyIntrospection(string token)
173-
{
174-
return new AsyncLazy<TokenIntrospectionResponse>(() => LoadClaimsForToken(token));
175-
}
176-
177-
private async Task<TokenIntrospectionResponse> LoadClaimsForToken(string token)
178+
private static async Task<TokenIntrospectionResponse> LoadClaimsForToken(string token, OAuth2IntrospectionOptions options)
178179
{
179-
var introspectionClient = await Options.IntrospectionClient.Value.ConfigureAwait(false);
180-
return await introspectionClient.Introspect(token, Options.TokenTypeHint).ConfigureAwait(false);
180+
var introspectionClient = await options.IntrospectionClient.Value.ConfigureAwait(false);
181+
return await introspectionClient.Introspect(token, options.TokenTypeHint).ConfigureAwait(false);
181182
}
182183

183-
private async Task<AuthenticateResult> CreateTicket(IEnumerable<Claim> claims, string token)
184+
private static async Task<AuthenticateResult> CreateTicket(
185+
IEnumerable<Claim> claims,
186+
string token,
187+
HttpContext httpContext,
188+
AuthenticationScheme scheme,
189+
OAuth2IntrospectionEvents events,
190+
OAuth2IntrospectionOptions options)
184191
{
185-
var authenticationType = Options.AuthenticationType ?? Scheme.Name;
186-
var id = new ClaimsIdentity(claims, authenticationType, Options.NameClaimType, Options.RoleClaimType);
192+
var authenticationType = options.AuthenticationType ?? scheme.Name;
193+
var id = new ClaimsIdentity(claims, authenticationType, options.NameClaimType, options.RoleClaimType);
187194
var principal = new ClaimsPrincipal(id);
188195

189-
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
196+
var tokenValidatedContext = new TokenValidatedContext(httpContext, scheme, options)
190197
{
191198
Principal = principal,
192199
SecurityToken = token
193200
};
194201

195-
await Events.TokenValidated(tokenValidatedContext);
202+
await events.TokenValidated(tokenValidatedContext);
196203
if (tokenValidatedContext.Result != null)
197204
{
198205
return tokenValidatedContext.Result;
199206
}
200207

201-
if (Options.SaveToken)
208+
if (options.SaveToken)
202209
{
203210
tokenValidatedContext.Properties.StoreTokens(new[]
204211
{

0 commit comments

Comments
 (0)