//-----------------------------------------------------------------------
//
// Copyright (c) Microsoft. All rights reserved.
//
//-----------------------------------------------------------------------
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;
///
/// Manage authenticating with an external OAuth or OpenID provider
///
public class OpenAuthSecurityManager {
#region Constants and Fields
///
/// Purposes string used for protecting the anti-XSRF token.
///
private const string AntiXsrfPurposeString = "DotNetOpenAuth.AspNet.AntiXsrfToken.v1";
///
/// The provider query string name.
///
private const string ProviderQueryStringName = "__provider__";
///
/// The query string name for session id.
///
private const string SessionIdQueryStringName = "__sid__";
///
/// The cookie name for session id.
///
private const string SessionIdCookieName = "__csid__";
///
/// The _authentication provider.
///
private readonly IAuthenticationClient authenticationProvider;
///
/// The _data provider.
///
private readonly IOpenAuthDataProvider dataProvider;
///
/// The _request context.
///
private readonly HttpContextBase requestContext;
#endregion
#region Constructors and Destructors
///
/// Initializes a new instance of the class.
///
///
/// The request context.
///
///
/// The provider.
///
///
/// The data provider.
///
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
///
/// Gets a value indicating whether IsAuthenticatedWithOpenAuth.
///
public bool IsAuthenticatedWithOpenAuth {
get {
return this.requestContext.Request.IsAuthenticated
&& OpenAuthAuthenticationTicketHelper.IsValidAuthenticationTicket(this.requestContext);
}
}
#endregion
#region Public Methods and Operators
///
/// Gets the provider that is responding to an authentication request.
///
///
/// The HTTP request context.
///
///
/// The provider name, if one is available.
///
public static string GetProviderName(HttpContextBase context) {
return context.Request.QueryString[ProviderQueryStringName];
}
///
/// Checks if the specified provider user id represents a valid account. If it does, log user in.
///
///
/// The provider user id.
///
///
/// if set to true create persistent cookie.
///
///
/// true if the login is successful.
///
[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;
}
///
/// Requests the specified provider to start the authentication by directing users to an external website
///
///
/// The return url after user is authenticated.
///
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);
}
///
/// Checks if user is successfully authenticated when user is redirected back to this user.
///
/// The return Url which must match exactly the Url passed into RequestAuthentication() earlier.
///
/// This returnUrl parameter only applies to OAuth2 providers. For other providers, it ignores the returnUrl parameter.
///
///
/// The result of the authentication.
///
public async Task 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);
}
}
///
/// Returns the username of the current logged-in user.
///
/// The HTTP request context.
/// The username, or String.Empty if anonymous.
private static string GetUsername(HttpContextBase context) {
string username = null;
if (context.User.Identity.IsAuthenticated) {
username = context.User.Identity.Name;
}
return username ?? string.Empty;
}
///
/// Validates the request against XSRF attack.
///
/// The session id embedded in the query string.
///
/// true if the request is safe. Otherwise, false.
///
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
}
}