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
|
//-----------------------------------------------------------------------
// <copyright file="OpenAuthSecurityManager.cs" company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.AspNet {
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Security;
using DotNetOpenAuth.AspNet.Clients;
using DotNetOpenAuth.Messaging;
using Validation;
/// <summary>
/// Manage authenticating with an external OAuth or OpenID provider
/// </summary>
public class OpenAuthSecurityManager {
#region Constants and Fields
/// <summary>
/// Purposes string used for protecting the anti-XSRF token.
/// </summary>
private const string AntiXsrfPurposeString = "DotNetOpenAuth.AspNet.AntiXsrfToken.v1";
/// <summary>
/// The provider query string name.
/// </summary>
private const string ProviderQueryStringName = "__provider__";
/// <summary>
/// The query string name for session id.
/// </summary>
private const string SessionIdQueryStringName = "__sid__";
/// <summary>
/// The cookie name for session id.
/// </summary>
private const string SessionIdCookieName = "__csid__";
/// <summary>
/// The _authentication provider.
/// </summary>
private readonly IAuthenticationClient authenticationProvider;
/// <summary>
/// The _data provider.
/// </summary>
private readonly IOpenAuthDataProvider dataProvider;
/// <summary>
/// The _request context.
/// </summary>
private readonly HttpContextBase requestContext;
#endregion
#region Constructors and Destructors
/// <summary>
/// Initializes a new instance of the <see cref="OpenAuthSecurityManager"/> class.
/// </summary>
/// <param name="requestContext">
/// The request context.
/// </param>
/// <param name="provider">
/// The provider.
/// </param>
/// <param name="dataProvider">
/// The data provider.
/// </param>
public OpenAuthSecurityManager(
HttpContextBase requestContext, IAuthenticationClient provider, IOpenAuthDataProvider dataProvider) {
Requires.NotNull(requestContext, "requestContext");
Requires.NotNull(provider, "provider");
Requires.NotNull(dataProvider, "dataProvider");
this.requestContext = requestContext;
this.dataProvider = dataProvider;
this.authenticationProvider = provider;
}
#endregion
#region Public Properties
/// <summary>
/// Gets a value indicating whether IsAuthenticatedWithOpenAuth.
/// </summary>
public bool IsAuthenticatedWithOpenAuth {
get {
return this.requestContext.Request.IsAuthenticated
&& OpenAuthAuthenticationTicketHelper.IsValidAuthenticationTicket(this.requestContext);
}
}
#endregion
#region Public Methods and Operators
/// <summary>
/// Gets the provider that is responding to an authentication request.
/// </summary>
/// <param name="context">
/// The HTTP request context.
/// </param>
/// <returns>
/// The provider name, if one is available.
/// </returns>
public static string GetProviderName(HttpContextBase context) {
return context.Request.QueryString[ProviderQueryStringName];
}
/// <summary>
/// Checks if the specified provider user id represents a valid account. If it does, log user in.
/// </summary>
/// <param name="providerUserId">
/// The provider user id.
/// </param>
/// <param name="createPersistentCookie">
/// if set to <c>true</c> create persistent cookie.
/// </param>
/// <returns>
/// <c>true</c> if the login is successful.
/// </returns>
[SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login",
Justification = "Login is used more consistently in ASP.Net")]
public bool Login(string providerUserId, bool createPersistentCookie) {
string userName = this.dataProvider.GetUserNameFromOpenAuth(
this.authenticationProvider.ProviderName, providerUserId);
if (string.IsNullOrEmpty(userName)) {
return false;
}
OpenAuthAuthenticationTicketHelper.SetAuthenticationTicket(this.requestContext, userName, createPersistentCookie);
return true;
}
/// <summary>
/// Requests the specified provider to start the authentication by directing users to an external website
/// </summary>
/// <param name="returnUrl">
/// The return url after user is authenticated.
/// </param>
public async Task RequestAuthenticationAsync(string returnUrl, CancellationToken cancellationToken = default(CancellationToken)) {
// convert returnUrl to an absolute path
Uri uri;
if (!string.IsNullOrEmpty(returnUrl)) {
uri = UriHelper.ConvertToAbsoluteUri(returnUrl, this.requestContext);
}
else {
uri = this.requestContext.Request.GetPublicFacingUrl();
}
// attach the provider parameter so that we know which provider initiated
// the login when user is redirected back to this page
uri = uri.AttachQueryStringParameter(ProviderQueryStringName, this.authenticationProvider.ProviderName);
// Guard against XSRF attack by injecting session id into the redirect url and response cookie.
// Upon returning from the external provider, we'll compare the session id value in the query
// string and the cookie. If they don't match, we'll reject the request.
string sessionId = Guid.NewGuid().ToString("N");
uri = uri.AttachQueryStringParameter(SessionIdQueryStringName, sessionId);
// The cookie value will be the current username secured against the session id we just created.
byte[] encryptedCookieBytes = MachineKeyUtil.Protect(Encoding.UTF8.GetBytes(GetUsername(this.requestContext)), AntiXsrfPurposeString, "Token: " + sessionId);
var xsrfCookie = new HttpCookie(SessionIdCookieName, HttpServerUtility.UrlTokenEncode(encryptedCookieBytes)) {
HttpOnly = true
};
if (FormsAuthentication.RequireSSL) {
xsrfCookie.Secure = true;
}
this.requestContext.Response.Cookies.Add(xsrfCookie);
// issue the redirect to the external auth provider
await this.authenticationProvider.RequestAuthenticationAsync(this.requestContext, uri, cancellationToken);
}
/// <summary>
/// Checks if user is successfully authenticated when user is redirected back to this user.
/// </summary>
/// <param name="returnUrl">The return Url which must match exactly the Url passed into RequestAuthentication() earlier.</param>
/// <remarks>
/// This returnUrl parameter only applies to OAuth2 providers. For other providers, it ignores the returnUrl parameter.
/// </remarks>
/// <returns>
/// The result of the authentication.
/// </returns>
public async Task<AuthenticationResult> VerifyAuthenticationAsync(string returnUrl, CancellationToken cancellationToken = default(CancellationToken)) {
// check for XSRF attack
string sessionId;
bool successful = this.ValidateRequestAgainstXsrfAttack(out sessionId);
if (!successful) {
return new AuthenticationResult(
isSuccessful: false,
provider: this.authenticationProvider.ProviderName,
providerUserId: null,
userName: null,
extraData: null);
}
// Only OAuth2 requires the return url value for the verify authenticaiton step
OAuth2Client oauth2Client = this.authenticationProvider as OAuth2Client;
if (oauth2Client != null) {
// convert returnUrl to an absolute path
Uri uri;
if (!string.IsNullOrEmpty(returnUrl)) {
uri = UriHelper.ConvertToAbsoluteUri(returnUrl, this.requestContext);
}
else {
uri = this.requestContext.Request.GetPublicFacingUrl();
}
// attach the provider parameter so that we know which provider initiated
// the login when user is redirected back to this page
uri = uri.AttachQueryStringParameter(ProviderQueryStringName, this.authenticationProvider.ProviderName);
// When we called RequestAuthentication(), we put the sessionId in the returnUrl query string.
// Hence, we need to put it in the VerifyAuthentication url again to please FB/Microsoft account providers.
uri = uri.AttachQueryStringParameter(SessionIdQueryStringName, sessionId);
try {
AuthenticationResult result = await oauth2Client.VerifyAuthenticationAsync(this.requestContext, uri, cancellationToken);
if (!result.IsSuccessful) {
// if the result is a Failed result, creates a new Failed response which has providerName info.
result = new AuthenticationResult(
isSuccessful: false,
provider: this.authenticationProvider.ProviderName,
providerUserId: null,
userName: null,
extraData: null);
}
return result;
}
catch (HttpException exception) {
return new AuthenticationResult(exception.GetBaseException(), this.authenticationProvider.ProviderName);
}
}
else {
return await this.authenticationProvider.VerifyAuthenticationAsync(this.requestContext, cancellationToken);
}
}
/// <summary>
/// Returns the username of the current logged-in user.
/// </summary>
/// <param name="context">The HTTP request context.</param>
/// <returns>The username, or String.Empty if anonymous.</returns>
private static string GetUsername(HttpContextBase context) {
string username = null;
if (context.User.Identity.IsAuthenticated) {
username = context.User.Identity.Name;
}
return username ?? string.Empty;
}
/// <summary>
/// Validates the request against XSRF attack.
/// </summary>
/// <param name="sessionId">The session id embedded in the query string.</param>
/// <returns>
/// <c>true</c> if the request is safe. Otherwise, <c>false</c>.
/// </returns>
private bool ValidateRequestAgainstXsrfAttack(out string sessionId) {
sessionId = null;
// get the session id query string parameter
string queryStringSessionId = this.requestContext.Request.QueryString[SessionIdQueryStringName];
// verify that the query string value is a valid guid
Guid guid;
if (!Guid.TryParse(queryStringSessionId, out guid)) {
return false;
}
// the cookie value should be the current username secured against this guid
var cookie = this.requestContext.Request.Cookies[SessionIdCookieName];
if (cookie == null || string.IsNullOrEmpty(cookie.Value)) {
return false;
}
// extract the username embedded within the cookie
// if there is any error at all (crypto, malformed, etc.), fail gracefully
string usernameInCookie = null;
try {
byte[] encryptedCookieBytes = HttpServerUtility.UrlTokenDecode(cookie.Value);
byte[] decryptedCookieBytes = MachineKeyUtil.Unprotect(encryptedCookieBytes, AntiXsrfPurposeString, "Token: " + queryStringSessionId);
usernameInCookie = Encoding.UTF8.GetString(decryptedCookieBytes);
}
catch {
return false;
}
string currentUsername = GetUsername(this.requestContext);
bool successful = string.Equals(currentUsername, usernameInCookie, StringComparison.OrdinalIgnoreCase);
if (successful) {
// be a good citizen, clean up cookie when the authentication succeeds
var xsrfCookie = new HttpCookie(SessionIdCookieName, string.Empty) {
HttpOnly = true,
Expires = DateTime.Now.AddYears(-1)
};
this.requestContext.Response.Cookies.Set(xsrfCookie);
}
sessionId = queryStringSessionId;
return successful;
}
#endregion
}
}
|