This repository has been archived by the owner on Jan 24, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
Copy pathFormsAuthentication.cs
390 lines (323 loc) · 15.4 KB
/
FormsAuthentication.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
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
namespace Nancy.Authentication.Forms
{
using System;
using Bootstrapper;
using Cookies;
using Cryptography;
using Extensions;
using Helpers;
using Security;
/// <summary>
/// Nancy forms authentication implementation
/// </summary>
public static class FormsAuthentication
{
private static string formsAuthenticationCookieName = "_ncfa";
// TODO - would prefer not to hold this here, but the redirect response needs it
private static FormsAuthenticationConfiguration currentConfiguration;
/// <summary>
/// Gets or sets the forms authentication cookie name
/// </summary>
public static string FormsAuthenticationCookieName
{
get
{
return formsAuthenticationCookieName;
}
set
{
formsAuthenticationCookieName = value;
}
}
/// <summary>
/// Enables forms authentication for the application
/// </summary>
/// <param name="pipelines">Pipelines to add handlers to (usually "this")</param>
/// <param name="configuration">Forms authentication configuration</param>
public static void Enable(IPipelines pipelines, FormsAuthenticationConfiguration configuration)
{
if (pipelines == null)
{
throw new ArgumentNullException("pipelines");
}
if (configuration == null)
{
throw new ArgumentNullException("configuration");
}
configuration.EnsureConfigurationIsValid();
currentConfiguration = configuration;
pipelines.BeforeRequest.AddItemToStartOfPipeline(GetLoadAuthenticationHook(configuration));
if (!configuration.DisableRedirect)
{
pipelines.AfterRequest.AddItemToEndOfPipeline(GetRedirectToLoginHook(configuration));
}
}
/// <summary>
/// Enables forms authentication for a module
/// </summary>
/// <param name="module">Module to add handlers to (usually "this")</param>
/// <param name="configuration">Forms authentication configuration</param>
public static void Enable(INancyModule module, FormsAuthenticationConfiguration configuration)
{
if (module == null)
{
throw new ArgumentNullException("module");
}
if (configuration == null)
{
throw new ArgumentNullException("configuration");
}
configuration.EnsureConfigurationIsValid();
module.RequiresAuthentication();
currentConfiguration = configuration;
module.Before.AddItemToStartOfPipeline(GetLoadAuthenticationHook(configuration));
if (!configuration.DisableRedirect)
{
module.After.AddItemToEndOfPipeline(GetRedirectToLoginHook(configuration));
}
}
/// <summary>
/// Creates a response that sets the authentication cookie and redirects
/// the user back to where they came from.
/// </summary>
/// <param name="context">Current context</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="fallbackRedirectUrl">Url to redirect to if none in the querystring</param>
/// <returns>Nancy response with redirect.</returns>
public static Response UserLoggedInRedirectResponse(NancyContext context, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = null)
{
var redirectUrl = fallbackRedirectUrl;
if (string.IsNullOrEmpty(redirectUrl))
{
redirectUrl = context.Request.Url.BasePath;
}
if (string.IsNullOrEmpty(redirectUrl))
{
redirectUrl = "/";
}
string redirectQuerystringKey = GetRedirectQuerystringKey(currentConfiguration);
if (context.Request.Query[redirectQuerystringKey].HasValue)
{
var queryUrl = (string)context.Request.Query[redirectQuerystringKey];
if (context.IsLocalUrl(queryUrl))
{
redirectUrl = queryUrl;
}
}
var response = context.GetRedirect(redirectUrl);
var authenticationCookie = BuildCookie(userIdentifier, cookieExpiry, currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Logs the user in.
/// </summary>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <returns>Nancy response with status <see cref="HttpStatusCode.OK"/></returns>
public static Response UserLoggedInResponse(Guid userIdentifier, DateTime? cookieExpiry = null)
{
var response =
(Response)HttpStatusCode.OK;
var authenticationCookie =
BuildCookie(userIdentifier, cookieExpiry, currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Logs the user out and redirects them to a URL
/// </summary>
/// <param name="context">Current context</param>
/// <param name="redirectUrl">URL to redirect to</param>
/// <returns>Nancy response</returns>
public static Response LogOutAndRedirectResponse(NancyContext context, string redirectUrl)
{
var response = context.GetRedirect(redirectUrl);
var authenticationCookie = BuildLogoutCookie(currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Logs the user out.
/// </summary>
/// <returns>Nancy response</returns>
public static Response LogOutResponse()
{
var response =
(Response)HttpStatusCode.OK;
var authenticationCookie =
BuildLogoutCookie(currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Gets the pre request hook for loading the authenticated user's details
/// from the cookie.
/// </summary>
/// <param name="configuration">Forms authentication configuration to use</param>
/// <returns>Pre request hook delegate</returns>
private static Func<NancyContext, Response> GetLoadAuthenticationHook(FormsAuthenticationConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException("configuration");
}
return context =>
{
var userGuid = GetAuthenticatedUserFromCookie(context, configuration);
if (userGuid != Guid.Empty)
{
context.CurrentUser = configuration.UserMapper.GetUserFromIdentifier(userGuid, context);
}
return null;
};
}
/// <summary>
/// Gets the post request hook for redirecting to the login page
/// </summary>
/// <param name="configuration">Forms authentication configuration to use</param>
/// <returns>Post request hook delegate</returns>
private static Action<NancyContext> GetRedirectToLoginHook(FormsAuthenticationConfiguration configuration)
{
return context =>
{
if (context.Response.StatusCode == HttpStatusCode.Unauthorized)
{
string redirectQuerystringKey = GetRedirectQuerystringKey(configuration);
context.Response = context.GetRedirect(
string.Format("{0}?{1}={2}",
configuration.RedirectUrl,
redirectQuerystringKey,
context.ToFullPath("~" + context.Request.Path + HttpUtility.UrlEncode(context.Request.Url.Query))));
}
};
}
/// <summary>
/// Gets the authenticated user GUID from the incoming request cookie if it exists
/// and is valid.
/// </summary>
/// <param name="context">Current context</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Returns user guid, or Guid.Empty if not present or invalid</returns>
private static Guid GetAuthenticatedUserFromCookie(NancyContext context, FormsAuthenticationConfiguration configuration)
{
string cookieValueEncrypted;
if (!context.Request.Cookies.TryGetValue(formsAuthenticationCookieName, out cookieValueEncrypted))
{
return Guid.Empty;
}
if (string.IsNullOrEmpty(cookieValueEncrypted))
{
return Guid.Empty;
}
var cookieValue = DecryptAndValidateAuthenticationCookie(cookieValueEncrypted, configuration);
Guid returnGuid;
if (string.IsNullOrEmpty(cookieValue) || !Guid.TryParse(cookieValue, out returnGuid))
{
return Guid.Empty;
}
return returnGuid;
}
/// <summary>
/// Build the forms authentication cookie
/// </summary>
/// <param name="userIdentifier">Authenticated user identifier</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Nancy cookie instance</returns>
private static INancyCookie BuildCookie(Guid userIdentifier, DateTime? cookieExpiry, FormsAuthenticationConfiguration configuration)
{
var cookieContents = EncryptAndSignCookie(userIdentifier.ToString(), configuration);
var cookie = new NancyCookie(formsAuthenticationCookieName, cookieContents, true, configuration.RequiresSSL, cookieExpiry);
if (!string.IsNullOrEmpty(configuration.Domain))
{
cookie.Domain = configuration.Domain;
}
if (!string.IsNullOrEmpty(configuration.Path))
{
cookie.Path = configuration.Path;
}
return cookie;
}
/// <summary>
/// Builds a cookie for logging a user out
/// </summary>
/// <param name="configuration">Current configuration</param>
/// <returns>Nancy cookie instance</returns>
private static INancyCookie BuildLogoutCookie(FormsAuthenticationConfiguration configuration)
{
var cookie = new NancyCookie(formsAuthenticationCookieName, String.Empty, true, configuration.RequiresSSL, DateTime.Now.AddDays(-1));
if (!string.IsNullOrEmpty(configuration.Domain))
{
cookie.Domain = configuration.Domain;
}
if (!string.IsNullOrEmpty(configuration.Path))
{
cookie.Path = configuration.Path;
}
return cookie;
}
/// <summary>
/// Encrypt and sign the cookie contents
/// </summary>
/// <param name="cookieValue">Plain text cookie value</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Encrypted and signed string</returns>
private static string EncryptAndSignCookie(string cookieValue, FormsAuthenticationConfiguration configuration)
{
var encryptedCookie = configuration.CryptographyConfiguration.EncryptionProvider.Encrypt(cookieValue);
var hmacBytes = GenerateHmac(encryptedCookie, configuration);
var hmacString = Convert.ToBase64String(hmacBytes);
return String.Format("{1}{0}", encryptedCookie, hmacString);
}
/// <summary>
/// Generate a hmac for the encrypted cookie string
/// </summary>
/// <param name="encryptedCookie">Encrypted cookie string</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Hmac byte array</returns>
private static byte[] GenerateHmac(string encryptedCookie, FormsAuthenticationConfiguration configuration)
{
return configuration.CryptographyConfiguration.HmacProvider.GenerateHmac(encryptedCookie);
}
/// <summary>
/// Decrypt and validate an encrypted and signed cookie value
/// </summary>
/// <param name="cookieValue">Encrypted and signed cookie value</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Decrypted value, or empty on error or if failed validation</returns>
public static string DecryptAndValidateAuthenticationCookie(string cookieValue, FormsAuthenticationConfiguration configuration)
{
var hmacStringLength = Base64Helpers.GetBase64Length(configuration.CryptographyConfiguration.HmacProvider.HmacLength);
var encryptedCookie = cookieValue.Substring(hmacStringLength);
var hmacString = cookieValue.Substring(0, hmacStringLength);
var encryptionProvider = configuration.CryptographyConfiguration.EncryptionProvider;
// Check the hmacs, but don't early exit if they don't match
var hmacBytes = Convert.FromBase64String(hmacString);
var newHmac = GenerateHmac(encryptedCookie, configuration);
var hmacValid = HmacComparer.Compare(newHmac, hmacBytes, configuration.CryptographyConfiguration.HmacProvider.HmacLength);
var decrypted = encryptionProvider.Decrypt(encryptedCookie);
// Only return the decrypted result if the hmac was ok
return hmacValid ? decrypted : string.Empty;
}
/// <summary>
/// Gets the redirect query string key from <see cref="FormsAuthenticationConfiguration"/>
/// </summary>
/// <param name="configuration">The forms authentication configuration.</param>
/// <returns>Redirect Querystring key</returns>
private static string GetRedirectQuerystringKey(FormsAuthenticationConfiguration configuration)
{
string redirectQuerystringKey = null;
if (configuration != null)
{
redirectQuerystringKey = configuration.RedirectQuerystringKey;
}
if (string.IsNullOrWhiteSpace(redirectQuerystringKey))
{
redirectQuerystringKey = FormsAuthenticationConfiguration.DefaultRedirectQuerystringKey;
}
return redirectQuerystringKey;
}
}
}