-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOAuth2IntrospectionHandler.cs
More file actions
267 lines (232 loc) · 11.1 KB
/
OAuth2IntrospectionHandler.cs
File metadata and controls
267 lines (232 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// 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 Duende.IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace IdentityModel.AspNetCore.OAuth2Introspection
{
/// <summary>
/// Authentication handler for OAuth 2.0 introspection
/// </summary>
public class OAuth2IntrospectionHandler : AuthenticationHandler<OAuth2IntrospectionOptions>
{
private readonly IDistributedCache _cache;
private readonly ILogger<OAuth2IntrospectionHandler> _logger;
private static readonly ConcurrentDictionary<string, Lazy<Task<TokenIntrospectionResponse>>> IntrospectionDictionary =
new ConcurrentDictionary<string, Lazy<Task<TokenIntrospectionResponse>>>();
/// <summary>
/// Initializes a new instance of the <see cref="OAuth2IntrospectionHandler"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="urlEncoder">The URL encoder.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="cache">The cache.</param>
public OAuth2IntrospectionHandler(
IOptionsMonitor<OAuth2IntrospectionOptions> options,
UrlEncoder urlEncoder,
ILoggerFactory loggerFactory,
IDistributedCache cache = null)
: base(options, loggerFactory, urlEncoder)
{
_logger = loggerFactory.CreateLogger<OAuth2IntrospectionHandler>();
_cache = cache;
}
/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
/// If it is not provided a default instance is supplied which does nothing when the methods are called.
/// </summary>
protected new OAuth2IntrospectionEvents Events
{
get => (OAuth2IntrospectionEvents)base.Events;
set => base.Events = value;
}
/// <inheritdoc/>
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new OAuth2IntrospectionEvents());
/// <summary>
/// Tries to authenticate a reference token on the current request
/// </summary>
/// <returns></returns>
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var token = Options.TokenRetriever(Context.Request);
// no token - nothing to do here
if (token.IsMissing())
{
return AuthenticateResult.NoResult();
}
// if token contains a dot - it might be a JWT and we are skipping
// this is configurable
if (token.Contains('.') && Options.SkipTokensWithDots)
{
_logger.LogTrace("Token contains a dot - skipped because SkipTokensWithDots is set.");
return AuthenticateResult.NoResult();
}
// if caching is enable - let's check if we have a cached introspection
if (Options.EnableCaching)
{
var claims = await _cache.GetClaimsAsync(Options, token).ConfigureAwait(false);
if (claims != null)
{
// find out if it is a cached inactive token
var isInActive = claims.FirstOrDefault(c => string.Equals(c.Type, "active", StringComparison.OrdinalIgnoreCase) && string.Equals(c.Value, "false", StringComparison.OrdinalIgnoreCase));
if (isInActive != null)
{
return await ReportNonSuccessAndReturn("Cached token is not active.", Context, Scheme, Events, Options);
}
return await CreateTicket(claims, token, Context, Scheme, Events, Options);
}
_logger.LogTrace("Token is not cached.");
}
// no cached result - let's make a network roundtrip to the introspection endpoint
// this code block tries to make sure that we only do a single roundtrip, even when multiple requests
// with the same token come in at the same time
try
{
Lazy<Task<TokenIntrospectionResponse>> GetTokenIntrospectionResponseLazy(string _)
{
return new Lazy<Task<TokenIntrospectionResponse>>(async () => await LoadClaimsForToken(token, Context, Scheme, Events, Options));
}
var response = await IntrospectionDictionary
.GetOrAdd(token, GetTokenIntrospectionResponseLazy)
.Value;
if (response.IsError)
{
_logger.LogError("Error returned from introspection endpoint: " + response.Error);
return await ReportNonSuccessAndReturn("Error returned from introspection endpoint: " + response.Error, Context, Scheme, Events, Options);
}
if (response.IsActive)
{
if (Options.EnableCaching)
{
await _cache.SetClaimsAsync(Options, token, response.Claims, Options.CacheDuration, _logger).ConfigureAwait(false);
}
return await CreateTicket(response.Claims, token, Context, Scheme, Events, Options);
}
else
{
if (Options.EnableCaching)
{
// add an exp claim - otherwise caching will not work
var claimsWithExp = response.Claims.ToList();
claimsWithExp.Add(new Claim("exp",
DateTimeOffset.UtcNow.Add(Options.CacheDuration).ToUnixTimeSeconds().ToString()));
await _cache.SetClaimsAsync(Options, token, claimsWithExp, Options.CacheDuration, _logger)
.ConfigureAwait(false);
}
return await ReportNonSuccessAndReturn("Token is not active.", Context, Scheme, Events, Options);
}
}
finally
{
IntrospectionDictionary.TryRemove(token, out _);
}
}
private static async Task<AuthenticateResult> ReportNonSuccessAndReturn(
string error,
HttpContext httpContext,
AuthenticationScheme scheme,
OAuth2IntrospectionEvents events,
OAuth2IntrospectionOptions options)
{
var authenticationFailedContext = new AuthenticationFailedContext(httpContext, scheme, options)
{
Error = error
};
await events.AuthenticationFailed(authenticationFailedContext);
return authenticationFailedContext.Result ?? AuthenticateResult.Fail(error);
}
private static async Task<TokenIntrospectionResponse> LoadClaimsForToken(
string token,
HttpContext context,
AuthenticationScheme scheme,
OAuth2IntrospectionEvents events,
OAuth2IntrospectionOptions options)
{
var introspectionClient = await options.IntrospectionClient.Value.ConfigureAwait(false);
using var request = CreateTokenIntrospectionRequest(token, context, scheme, events, options);
var requestSendingContext = new SendingRequestContext(context, scheme, options)
{
TokenIntrospectionRequest = request,
};
await events.SendingRequest(requestSendingContext);
return await introspectionClient.IntrospectTokenAsync(request, context.RequestAborted).ConfigureAwait(false);
}
private static TokenIntrospectionRequest CreateTokenIntrospectionRequest(
string token,
HttpContext context,
AuthenticationScheme scheme,
OAuth2IntrospectionEvents events,
OAuth2IntrospectionOptions options)
{
if (options.ClientSecret == null && options.ClientAssertionExpirationTime <= DateTime.UtcNow)
{
lock (options.AssertionUpdateLockObj)
{
if (options.ClientAssertionExpirationTime <= DateTime.UtcNow)
{
var updateClientAssertionContext = new UpdateClientAssertionContext(context, scheme, options)
{
ClientAssertion = options.ClientAssertion ?? new ClientAssertion()
};
events.UpdateClientAssertion(updateClientAssertionContext);
options.ClientAssertion = updateClientAssertionContext.ClientAssertion;
options.ClientAssertionExpirationTime =
updateClientAssertionContext.ClientAssertionExpirationTime;
}
}
}
return new TokenIntrospectionRequest
{
Token = token,
TokenTypeHint = options.TokenTypeHint,
Address = options.IntrospectionEndpoint,
ClientId = options.ClientId,
ClientSecret = options.ClientSecret,
ClientAssertion = options.ClientAssertion ?? new ClientAssertion(),
ClientCredentialStyle = options.ClientCredentialStyle,
AuthorizationHeaderStyle = options.AuthorizationHeaderStyle,
};
}
private static async Task<AuthenticateResult> CreateTicket(
IEnumerable<Claim> claims,
string token,
HttpContext httpContext,
AuthenticationScheme scheme,
OAuth2IntrospectionEvents events,
OAuth2IntrospectionOptions options)
{
var authenticationType = options.AuthenticationType ?? scheme.Name;
var id = new ClaimsIdentity(claims, authenticationType, options.NameClaimType, options.RoleClaimType);
var principal = new ClaimsPrincipal(id);
var tokenValidatedContext = new TokenValidatedContext(httpContext, scheme, options)
{
Principal = principal,
SecurityToken = token
};
await events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}
if (options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}
tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}
}
}