-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathRequestAuthenticationService.cs
352 lines (306 loc) · 11.3 KB
/
RequestAuthenticationService.cs
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OrderCloud.SDK;
using System.IO;
using System.Text;
namespace OrderCloud.Catalyst
{
/// <summary>
/// Injectable service to aid with getting, decoding, and verifying OrderCloud auth tokens on an HttpRequest.
/// </summary>
public class RequestAuthenticationService
{
private readonly ISimpleCache _cache;
private readonly IOrderCloudClient _oc;
private readonly IHttpContextAccessor _httpContextAccessor;
public RequestAuthenticationService(ISimpleCache cache, IOrderCloudClient oc, IHttpContextAccessor httpContextAccessor)
{
_cache = cache;
_oc = oc;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// Get a raw OrderCloud token from the provided request headers
/// </summary>
public static string GetToken(HttpRequest request)
{
if (!request.Headers.TryGetValue("Authorization", out var header))
return null;
var parts = header.FirstOrDefault()?.Split(new[] { ' ' }, 2);
if (parts?.Length != 2)
return null;
if (parts[0] != "Bearer")
return null;
var accessToken = parts[1].Trim();
Require.That(!string.IsNullOrEmpty(accessToken), new UnAuthorizedException());
return accessToken;
}
/// <summary>
/// Get the header "X-oc-hash" of the provided request. Used to verify the request originated from OrderCloud.
/// </summary>
public static string GetWebhookHash(HttpRequest request)
{
var sentHash = request.Headers?["X-oc-hash"].FirstOrDefault();
Require.That(!string.IsNullOrEmpty(sentHash), new WebhookUnauthorizedException());
return sentHash;
}
/// <summary>
/// Get the header "X-oc-hash" of the current HttpContext Request. Used to verify the request originated from OrderCloud.
/// </summary>
public string GetWebhookHash()
{
return GetWebhookHash(_httpContextAccessor.HttpContext.Request);
}
/// <summary>
/// Get a raw OrderCloud token from the current HttpContext request headers
/// </summary>
public string GetToken()
{
return GetToken(_httpContextAccessor.HttpContext.Request);
}
/// <summary>
/// Get a strongly typed model of the OrderCloud token from the provided request headers
/// </summary>
public static DecodedToken GetDecodedToken(HttpRequest request)
{
var token = GetToken(request);
return new DecodedToken(token);
}
/// <summary>
/// Get a strongly typed model of the OrderCloud token from the current HttpContext request headers
/// </summary>
public DecodedToken GetDecodedToken()
{
return GetDecodedToken(_httpContextAccessor.HttpContext.Request);
}
/// <summary>
/// Verify the provided OrderCloud token. Throws 401 if invalid or 403 if insufficient roles.
/// </summary>
public async Task<DecodedToken> VerifyTokenAsync(string token, OrderCloudUserAuthOptions options, IEnumerable<string> requiredRoles = null, IEnumerable<CommerceRole> allowedUserTypes = null)
{
Require.That(!string.IsNullOrEmpty(token), new UnAuthorizedException());
var decodedToken = new DecodedToken(token);
Require.That(decodedToken.ClientID != null, new UnAuthorizedException());
Require.That(options.AnyClientIDCanAccess || options.ValidClientIDs.Contains(decodedToken.ClientID, StringComparer.InvariantCultureIgnoreCase), new UnAuthorizedException());
Require.That(decodedToken.NotValidBeforeUTC < DateTime.UtcNow && decodedToken.ExpiresUTC > DateTime.UtcNow,
new UnAuthorizedException());
// we've validated the token as much as we can on this end, go make sure it's ok on OC
bool isValid;
// some valid tokens - e.g. those from the portal - do not have a "kid"
if (decodedToken.KeyID == null)
{
isValid = await VerifyTokenWithMeGet(decodedToken); // also sets meUser field;
}
else
{
isValid = await VerifyTokenWithKeyID(decodedToken);
}
if (!isValid)
{
Require.That(decodedToken.ApiUrl == _oc?.Config?.ApiUrl,
new WrongEnvironmentException(new WrongEnvironmentError()
{
ExpectedEnvironment = _oc?.Config?.ApiUrl,
TokenIssuerEnvironment = decodedToken.ApiUrl
}
));
}
Require.That(isValid, new UnAuthorizedException());
Require.That(allowedUserTypes.IsNullOrEmpty() || allowedUserTypes.Contains(decodedToken.CommerceRole),
new InvalidUserTypeException(new InvalidUserTypeError()
{
ThisUserType = decodedToken?.CommerceRole.ToString(),
UserTypesThatCanAccess = allowedUserTypes?.Select(x => x.ToString())?.ToList()
})
);
Require.That(requiredRoles.IsNullOrEmpty() || requiredRoles.Any(role => decodedToken.Roles.Contains(role)),
new InsufficientRolesException(new InsufficientRolesError()
{
SufficientRoles = requiredRoles?.ToList(),
AssignedRoles = decodedToken.Roles.ToList()
})
);
return decodedToken;
}
/// <summary>
/// Verify the provided HttpRequest's OrderCloud Token. Throws 401 if invalid or 403 if insufficient roles.
/// </summary>
public async Task<DecodedToken> VerifyTokenAsync(HttpRequest request, OrderCloudUserAuthOptions options, IEnumerable<string> requiredRoles = null, IEnumerable<CommerceRole> allowedUserTypes = null)
{
var token = GetToken(request);
return await VerifyTokenAsync(token, options, requiredRoles, allowedUserTypes);
}
/// <summary>
/// Verify the current HttpContext request's OrderCloud token. Throws 401 if invalid or 403 if insufficient roles.
/// </summary>
public async Task<DecodedToken> VerifyTokenAsync(OrderCloudUserAuthOptions options, IEnumerable<string> requiredRoles = null, IEnumerable<CommerceRole> allowedUserTypes = null)
{
return await VerifyTokenAsync(_httpContextAccessor.HttpContext.Request, options, requiredRoles, allowedUserTypes);
}
/// <summary>
/// Verify the provided webhook hash. Proves the request originated from OrderCloud.
/// </summary>
public bool VerifyWebhookHashAsync(string requestHash, string requestBody, OrderCloudWebhookAuthOptions options)
{
Require.That(!string.IsNullOrEmpty(options.HashKey),
new InvalidOperationException("OrderCloudWebhookAuthOptions.HashKey was not configured."));
Require.That(!string.IsNullOrEmpty(requestBody), new WebhookUnauthorizedException());
Require.That(!string.IsNullOrEmpty(requestHash), new WebhookUnauthorizedException());
var bodyBytes = Encoding.UTF8.GetBytes(requestBody);
var keyBytes = Encoding.UTF8.GetBytes(options.HashKey);
var hash = new HMACSHA256(keyBytes).ComputeHash(bodyBytes);
var computed = Convert.ToBase64String(hash);
Require.That(requestHash == computed, new WebhookUnauthorizedException());
return true;
}
/// <summary>
/// Verify the provided webhook hash. Proves the request originated from OrderCloud.
/// </summary>
public async Task<bool> VerifyWebhookHashAsync(string requestHash, HttpRequest request, OrderCloudWebhookAuthOptions options)
{
var requestBody = await GetHttpRequestBody(request);
return VerifyWebhookHashAsync(requestHash, requestBody, options);
}
/// <summary>
/// Verify the provided HttpContext request's webhook hash. Proves the request came from OrderCloud.
/// </summary>
public async Task<bool> VerifyWebhookHashAsync(HttpRequest request, OrderCloudWebhookAuthOptions options)
{
var requestHash = GetWebhookHash(request);
return await VerifyWebhookHashAsync(requestHash, request, options);
}
/// <summary>
/// Verify the current HttpContext request's webhook hash. Proves the request came from OrderCloud.
/// </summary>
public async Task<bool> VerifyWebhookHashAsync(OrderCloudWebhookAuthOptions options)
{
return await VerifyWebhookHashAsync(_httpContextAccessor.HttpContext.Request, options);
}
/// <summary>
/// This still won't work inside a controller unless there's middleware to run request.EnableBuffering();
/// See https://stackoverflow.com/questions/59185410/request-body-from-is-empty-in-net-core-3-0
/// </summary>
private async Task<string> GetHttpRequestBody(HttpRequest request)
{
request.EnableBuffering();
request.Body.Position = 0;
try
{
return await new StreamReader(request.Body).ReadToEndAsync();
}
finally
{
request.Body.Position = 0;
}
}
/// <summary>
/// Get the full details of the currently authenticated user based on the HttpContext request token
/// </summary>
public async Task<T> GetUserAsync<T>()
where T : MeUser
{
var token = GetToken();
return await _oc.Me.GetAsync<T>(token);
}
/// <summary>
/// Get the full details of the currently authenticated user based on the HttpContext request token
/// </summary>
public async Task<MeUser> GetUserAsync()
{
var token = GetToken();
return await _oc.Me.GetAsync(token);
}
/// <summary>
/// Get an IOrderCloudClient with token set based on the HttpContext request
/// </summary>
public IOrderCloudClient BuildClient()
{
return GetDecodedToken().BuildClient();
}
/// <summary>
/// Verifiy the validity of an OrderCloud token, given details about the public key.
/// </summary>
public static bool VerifyTokenWithPublicKey(string accessToken, PublicKey publicKey)
{
if (publicKey == null)
{
return false;
}
var rsa = new RSACryptoServiceProvider(2048);
rsa.ImportParameters(new RSAParameters
{
Modulus = FromBase64Url(publicKey.n),
Exponent = FromBase64Url(publicKey.e)
});
var rsaSecurityKey = new RsaSecurityKey(rsa);
var result = new JsonWebTokenHandler().ValidateToken(accessToken, new TokenValidationParameters
{
IssuerSigningKey = rsaSecurityKey,
RequireSignedTokens = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
LifetimeValidator = (nbf, exp, _, __) => nbf < DateTime.UtcNow && exp > DateTime.UtcNow,
ValidateIssuer = false,
RequireExpirationTime = true,
ValidateAudience = false
});
return result.IsValid;
}
private async Task<bool> VerifyTokenWithMeGet(DecodedToken jwt)
{
var cacheKey = jwt.AccessToken;
return await _cache.GetOrAddAsync(cacheKey, TimeSpan.FromHours(1), async () =>
{
try
{
var meUser = await _oc.Me.GetAsync(jwt.AccessToken);
return meUser != null && meUser.Active;
}
catch (OrderCloudException ex)
{
throw ex;
}
catch (Exception ex)
{
await _cache.RemoveAsync(cacheKey); // not their fault, don't make them wait 1 hr
return false;
}
});
}
private async Task<bool> VerifyTokenWithKeyID(DecodedToken jwt)
{
var cacheKey = $"{jwt.ApiUrl}-{jwt.KeyID}";
var publicKey = await _cache.GetOrAddAsync(cacheKey, TimeSpan.FromDays(30), async () =>
{
try
{
return await _oc.Certs.GetPublicKeyAsync(jwt.KeyID);
}
catch (OrderCloudException ex)
{
throw ex;
}
catch (Exception ex)
{
await _cache.RemoveAsync(cacheKey); // not their fault, don't make them wait 5 min
return null; // null public key will lead to unauthorized exception;
}
});
return VerifyTokenWithPublicKey(jwt.AccessToken, publicKey);
}
private static byte[] FromBase64Url(string base64Url)
{
string padded = base64Url.Length % 4 == 0
? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
string base64 = padded.Replace("_", "/")
.Replace("-", "+");
return Convert.FromBase64String(base64);
}
}
}