#49 - OAuth base middleware.

This commit is contained in:
Chris Ross 2014-09-03 11:48:38 -07:00
parent 214b82ea01
commit 80c8891c08
52 changed files with 1519 additions and 1557 deletions

View File

@ -32,6 +32,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.T
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.MicrosoftAccount", "src\Microsoft.AspNet.Security.MicrosoftAccount\Microsoft.AspNet.Security.MicrosoftAccount.kproj", "{1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.OAuth", "src\Microsoft.AspNet.Security.OAuth\Microsoft.AspNet.Security.OAuth.kproj", "{4A636011-68EE-4CE5-836D-EA8E13CF71E4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -132,6 +134,16 @@ Global
{1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Release|x86.ActiveCfg = Release|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Debug|x86.ActiveCfg = Debug|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Release|Any CPU.Build.0 = Release|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -146,5 +158,6 @@ Global
{89BF8535-A849-458E-868A-A68FCF620486} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
{C96B77EA-4078-4C31-BDB2-878F11C5E061} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
{1FCF26C2-A3C7-4308-B698-4AFC3560BC0C} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
{4A636011-68EE-4CE5-836D-EA8E13CF71E4} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
EndGlobalSection
EndGlobal

View File

@ -1,11 +1,19 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security;
using Microsoft.AspNet.Security.Cookies;
using Microsoft.AspNet.Security.Facebook;
using Microsoft.AspNet.Security.Google;
using Microsoft.AspNet.Security.MicrosoftAccount;
using Microsoft.AspNet.Security.OAuth;
using Microsoft.AspNet.Security.Twitter;
using Newtonsoft.Json.Linq;
namespace CookieSample
{
@ -28,6 +36,16 @@ namespace CookieSample
AppSecret = "a124463c4719c94b4228d9a240e5dc1a",
});
app.UseOAuthAuthentication(new OAuthAuthenticationOptions<IOAuthAuthenticationNotifications>("Google-AccessToken")
{
ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f",
CallbackPath = new PathString("/signin-google-token"),
AuthorizationEndpoint = GoogleAuthenticationDefaults.AuthorizationEndpoint,
TokenEndpoint = GoogleAuthenticationDefaults.TokenEndpoint,
Scope = { "openid", "profile", "email" },
});
app.UseGoogleAuthentication(new GoogleAuthenticationOptions()
{
ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
@ -57,6 +75,17 @@ namespace CookieSample
The sample app can then be run via:
k web
*/
app.UseOAuthAuthentication(new OAuthAuthenticationOptions<IOAuthAuthenticationNotifications>("Microsoft-AccessToken")
{
Caption = "MicrosoftAccount-AccessToken - Requires project changes",
ClientId = "00000000480FF62E",
ClientSecret = "bLw2JIvf8Y1TaToipPEqxTVlOeJwCUsr",
CallbackPath = new PathString("/signin-microsoft-token"),
AuthorizationEndpoint = MicrosoftAccountAuthenticationDefaults.AuthorizationEndpoint,
TokenEndpoint = MicrosoftAccountAuthenticationDefaults.TokenEndpoint,
Scope = { "wl.basic" },
});
app.UseMicrosoftAccountAuthentication(new MicrosoftAccountAuthenticationOptions()
{
Caption = "MicrosoftAccount - Requires project changes",
@ -64,6 +93,70 @@ namespace CookieSample
ClientSecret = "bLw2JIvf8Y1TaToipPEqxTVlOeJwCUsr",
});
app.UseOAuthAuthentication(new OAuthAuthenticationOptions<IOAuthAuthenticationNotifications>("GitHub-AccessToken")
{
ClientId = "8c0c5a572abe8fe89588",
ClientSecret = "e1d95eaf03461d27acd6f49d4fc7bf19d6ac8cda",
CallbackPath = new PathString("/signin-github-token"),
AuthorizationEndpoint = "https://github.com/login/oauth/authorize",
TokenEndpoint = "https://github.com/login/oauth/access_token",
});
app.UseOAuthAuthentication(new OAuthAuthenticationOptions<IOAuthAuthenticationNotifications>("GitHub")
{
ClientId = "49e302895d8b09ea5656",
ClientSecret = "98f1bf028608901e9df91d64ee61536fe562064b",
CallbackPath = new PathString("/signin-github"),
AuthorizationEndpoint = "https://github.com/login/oauth/authorize",
TokenEndpoint = "https://github.com/login/oauth/access_token",
UserInformationEndpoint = "https://api.github.com/user",
// Retrieving user information is unique to each provider.
Notifications = new OAuthAuthenticationNotifications()
{
OnGetUserInformationAsync = async (context) =>
{
// Get the GitHub user
HttpRequestMessage userRequest = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
userRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
userRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
userRequest.Headers.UserAgent.ParseAdd("Microsoft ASP.NET OAuth middleware for GitHub");
HttpResponseMessage userResponse = await context.Backchannel.SendAsync(userRequest, context.HttpContext.RequestAborted);
userResponse.EnsureSuccessStatusCode();
var text = await userResponse.Content.ReadAsStringAsync();
JObject user = JObject.Parse(text);
var identity = new ClaimsIdentity(
context.Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
JToken value;
var id = user.TryGetValue("id", out value) ? value.ToString() : null;
if (!string.IsNullOrEmpty(id))
{
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id, ClaimValueTypes.String, context.Options.AuthenticationType));
}
var userName = user.TryGetValue("login", out value) ? value.ToString() : null;
if (!string.IsNullOrEmpty(userName))
{
identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, userName, ClaimValueTypes.String, context.Options.AuthenticationType));
}
var name = user.TryGetValue("name", out value) ? value.ToString() : null;
if (!string.IsNullOrEmpty(name))
{
identity.AddClaim(new Claim("urn:github:name", name, ClaimValueTypes.String, context.Options.AuthenticationType));
}
var link = user.TryGetValue("url", out value) ? value.ToString() : null;
if (!string.IsNullOrEmpty(link))
{
identity.AddClaim(new Claim("urn:github:url", link, ClaimValueTypes.String, context.Options.AuthenticationType));
}
context.Identity = identity;
},
},
});
// Choose an authentication type
app.Map("/login", signoutApp =>
{

View File

@ -6,5 +6,11 @@ namespace Microsoft.AspNet.Security.Facebook
public static class FacebookAuthenticationDefaults
{
public const string AuthenticationType = "Facebook";
public const string AuthorizationEndpoint = "https://www.facebook.com/dialog/oauth";
public const string TokenEndpoint = "https://graph.facebook.com/oauth/access_token";
public const string UserInformationEndpoint = "https://graph.facebook.com/me";
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Security.Claims;
@ -11,269 +10,94 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Infrastructure;
using Microsoft.AspNet.Security.OAuth;
using Microsoft.AspNet.WebUtilities;
using Microsoft.Framework.Logging;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.Facebook
{
internal class FacebookAuthenticationHandler : AuthenticationHandler<FacebookAuthenticationOptions>
internal class FacebookAuthenticationHandler : OAuthAuthenticationHandler<FacebookAuthenticationOptions, IFacebookAuthenticationNotifications>
{
private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";
private const string TokenEndpoint = "https://graph.facebook.com/oauth/access_token";
private const string GraphApiEndpoint = "https://graph.facebook.com/me";
private const string AuthorizationEndpoint = "https://www.facebook.com/dialog/oauth";
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
public FacebookAuthenticationHandler(HttpClient httpClient, ILogger logger)
: base(httpClient, logger)
{
_httpClient = httpClient;
_logger = logger;
}
protected override AuthenticationTicket AuthenticateCore()
protected override async Task<TokenResponse> ExchangeCodeAsync(string code, string redirectUri)
{
return AuthenticateCoreAsync().Result;
var queryBuilder = new QueryBuilder()
{
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", redirectUri },
{ "client_id", Options.AppId },
{ "client_secret", Options.AppSecret },
};
var tokenResponse = await Backchannel.GetAsync(Options.TokenEndpoint + queryBuilder.ToString(), Context.RequestAborted);
tokenResponse.EnsureSuccessStatusCode();
string oauthTokenResponse = await tokenResponse.Content.ReadAsStringAsync();
IFormCollection form = FormHelpers.ParseForm(oauthTokenResponse);
var response = new JObject();
foreach (string key in form.Keys)
{
response.Add(string.Equals(key, "expires", StringComparison.OrdinalIgnoreCase) ? "expires_in" : key, form[key]);
}
// The refresh token is not available.
return new TokenResponse(response);
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
protected override async Task<AuthenticationTicket> GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens)
{
AuthenticationProperties properties = null;
try
string graphAddress = Options.UserInformationEndpoint + "?access_token=" + Uri.EscapeDataString(tokens.AccessToken);
if (Options.SendAppSecretProof)
{
string code = null;
string state = null;
IReadableStringCollection query = Request.Query;
IList<string> values = query.GetValues("error");
if (values != null && values.Count >= 1)
{
_logger.WriteVerbose("Remote server returned an error: " + Request.QueryString);
}
values = query.GetValues("code");
if (values != null && values.Count == 1)
{
code = values[0];
}
values = query.GetValues("state");
if (values != null && values.Count == 1)
{
state = values[0];
}
properties = Options.StateDataFormat.Unprotect(state);
if (properties == null)
{
return null;
}
// OAuth2 10.12 CSRF
if (!ValidateCorrelationId(properties, _logger))
{
return new AuthenticationTicket(null, properties);
}
if (code == null)
{
// Null if the remote server returns an error.
return new AuthenticationTicket(null, properties);
}
string requestPrefix = Request.Scheme + "://" + Request.Host;
string redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath;
string tokenRequest = "grant_type=authorization_code" +
"&code=" + Uri.EscapeDataString(code) +
"&redirect_uri=" + Uri.EscapeDataString(redirectUri) +
"&client_id=" + Uri.EscapeDataString(Options.AppId) +
"&client_secret=" + Uri.EscapeDataString(Options.AppSecret);
var tokenResponse = await _httpClient.GetAsync(TokenEndpoint + "?" + tokenRequest, Context.RequestAborted);
tokenResponse.EnsureSuccessStatusCode();
string text = await tokenResponse.Content.ReadAsStringAsync();
IFormCollection form = FormHelpers.ParseForm(text);
string accessToken = form["access_token"];
string expires = form["expires"];
string graphAddress = GraphApiEndpoint + "?access_token=" + Uri.EscapeDataString(accessToken);
if (Options.SendAppSecretProof)
{
graphAddress += "&appsecret_proof=" + GenerateAppSecretProof(accessToken);
}
var graphResponse = await _httpClient.GetAsync(graphAddress, Context.RequestAborted);
graphResponse.EnsureSuccessStatusCode();
text = await graphResponse.Content.ReadAsStringAsync();
JObject user = JObject.Parse(text);
var context = new FacebookAuthenticatedContext(Context, user, accessToken, expires);
context.Identity = new ClaimsIdentity(
Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if (!string.IsNullOrEmpty(context.Id))
{
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.UserName))
{
context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName, XmlSchemaString, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Email))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Name))
{
context.Identity.AddClaim(new Claim("urn:facebook:name", context.Name, XmlSchemaString, Options.AuthenticationType));
// Many Facebook accounts do not set the UserName field. Fall back to the Name field instead.
if (string.IsNullOrEmpty(context.UserName))
{
context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.Name, XmlSchemaString, Options.AuthenticationType));
}
}
if (!string.IsNullOrEmpty(context.Link))
{
context.Identity.AddClaim(new Claim("urn:facebook:link", context.Link, XmlSchemaString, Options.AuthenticationType));
}
context.Properties = properties;
await Options.Notifications.Authenticated(context);
return new AuthenticationTicket(context.Identity, context.Properties);
}
catch (Exception ex)
{
_logger.WriteError("Authentication failed", ex);
return new AuthenticationTicket(null, properties);
}
}
protected override void ApplyResponseChallenge()
{
if (Response.StatusCode != 401)
{
return;
graphAddress += "&appsecret_proof=" + GenerateAppSecretProof(tokens.AccessToken);
}
// Active middleware should redirect on 401 even if there wasn't an explicit challenge.
if (ChallengeContext == null && Options.AuthenticationMode == AuthenticationMode.Passive)
var graphResponse = await Backchannel.GetAsync(graphAddress, Context.RequestAborted);
graphResponse.EnsureSuccessStatusCode();
string text = await graphResponse.Content.ReadAsStringAsync();
JObject user = JObject.Parse(text);
var context = new FacebookAuthenticatedContext(Context, Options, user, tokens);
context.Identity = new ClaimsIdentity(
Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if (!string.IsNullOrEmpty(context.Id))
{
return;
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, ClaimValueTypes.String, Options.AuthenticationType));
}
string baseUri =
Request.Scheme +
"://" +
Request.Host +
Request.PathBase;
string currentUri =
baseUri +
Request.Path +
Request.QueryString;
string redirectUri =
baseUri +
Options.CallbackPath;
AuthenticationProperties properties;
if (ChallengeContext == null)
if (!string.IsNullOrEmpty(context.UserName))
{
properties = new AuthenticationProperties();
context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName, ClaimValueTypes.String, Options.AuthenticationType));
}
else
if (!string.IsNullOrEmpty(context.Email))
{
properties = new AuthenticationProperties(ChallengeContext.Properties);
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, Options.AuthenticationType));
}
if (string.IsNullOrEmpty(properties.RedirectUri))
if (!string.IsNullOrEmpty(context.Name))
{
properties.RedirectUri = currentUri;
}
context.Identity.AddClaim(new Claim("urn:facebook:name", context.Name, ClaimValueTypes.String, Options.AuthenticationType));
// OAuth2 10.12 CSRF
GenerateCorrelationId(properties);
// comma separated
string scope = string.Join(",", Options.Scope);
string state = Options.StateDataFormat.Protect(properties);
string authorizationEndpoint =
AuthorizationEndpoint +
"?response_type=code" +
"&client_id=" + Uri.EscapeDataString(Options.AppId) +
"&redirect_uri=" + Uri.EscapeDataString(redirectUri) +
"&scope=" + Uri.EscapeDataString(scope) +
"&state=" + Uri.EscapeDataString(state);
var redirectContext = new FacebookApplyRedirectContext(Context, Options, properties, authorizationEndpoint);
Options.Notifications.ApplyRedirect(redirectContext);
}
protected override void ApplyResponseGrant()
{
// N/A
}
public override async Task<bool> InvokeAsync()
{
return await InvokeReplyPathAsync();
}
private async Task<bool> InvokeReplyPathAsync()
{
if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
{
// TODO: error responses
AuthenticationTicket ticket = await AuthenticateAsync();
if (ticket == null)
// Many Facebook accounts do not set the UserName field. Fall back to the Name field instead.
if (string.IsNullOrEmpty(context.UserName))
{
_logger.WriteWarning("Invalid return state, unable to redirect.");
Response.StatusCode = 500;
return true;
context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.Name, ClaimValueTypes.String, Options.AuthenticationType));
}
var context = new FacebookReturnEndpointContext(Context, ticket);
context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType;
context.RedirectUri = ticket.Properties.RedirectUri;
await Options.Notifications.ReturnEndpoint(context);
if (context.SignInAsAuthenticationType != null &&
context.Identity != null)
{
ClaimsIdentity grantIdentity = context.Identity;
if (!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
{
grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType);
}
Context.Response.SignIn(context.Properties, grantIdentity);
}
if (!context.IsRequestCompleted && context.RedirectUri != null)
{
string redirectUri = context.RedirectUri;
if (context.Identity == null)
{
// add a redirect hint that sign-in failed in some way
redirectUri = QueryHelpers.AddQueryString(redirectUri, "error", "access_denied");
}
Response.Redirect(redirectUri);
context.RequestCompleted();
}
return context.IsRequestCompleted;
}
return false;
if (!string.IsNullOrEmpty(context.Link))
{
context.Identity.AddClaim(new Claim("urn:facebook:link", context.Link, ClaimValueTypes.String, Options.AuthenticationType));
}
context.Properties = properties;
await Options.Notifications.Authenticated(context);
return new AuthenticationTicket(context.Identity, context.Properties);
}
private string GenerateAppSecretProof(string accessToken)
@ -289,5 +113,13 @@ namespace Microsoft.AspNet.Security.Facebook
return builder.ToString();
}
}
protected override string FormatScope()
{
// Facebook deviates from the OAuth spec here. They require comma separated instead of space separated.
// https://developers.facebook.com/docs/reference/dialogs/oauth
// http://tools.ietf.org/html/rfc6749#section-3.3
return string.Join(",", Options.Scope);
}
}
}

View File

@ -2,13 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Security.DataHandler;
using Microsoft.AspNet.Security.DataProtection;
using Microsoft.AspNet.Security.Infrastructure;
using Microsoft.AspNet.Security.OAuth;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Security.Facebook
@ -16,12 +14,8 @@ namespace Microsoft.AspNet.Security.Facebook
/// <summary>
/// An ASP.NET middleware for authenticating users using Facebook.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware is not disposable.")]
public class FacebookAuthenticationMiddleware : AuthenticationMiddleware<FacebookAuthenticationOptions>
public class FacebookAuthenticationMiddleware : OAuthAuthenticationMiddleware<FacebookAuthenticationOptions, IFacebookAuthenticationNotifications>
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
/// <summary>
/// Initializes a new <see cref="FacebookAuthenticationMiddleware"/>.
/// </summary>
@ -34,7 +28,7 @@ namespace Microsoft.AspNet.Security.Facebook
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
FacebookAuthenticationOptions options)
: base(next, options)
: base(next, dataProtectionProvider, loggerFactory, options)
{
if (string.IsNullOrWhiteSpace(Options.AppId))
{
@ -45,22 +39,10 @@ namespace Microsoft.AspNet.Security.Facebook
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "AppSecret"));
}
_logger = loggerFactory.Create(typeof(FacebookAuthenticationMiddleware).FullName);
if (Options.Notifications == null)
{
Options.Notifications = new FacebookAuthenticationNotifications();
}
if (Options.StateDataFormat == null)
{
IDataProtector dataProtector = DataProtectionHelpers.CreateDataProtector(dataProtectionProvider,
typeof(FacebookAuthenticationMiddleware).FullName, options.AuthenticationType, "v1");
Options.StateDataFormat = new PropertiesDataFormat(dataProtector);
}
_httpClient = new HttpClient(ResolveHttpMessageHandler(Options));
_httpClient.Timeout = Options.BackchannelTimeout;
_httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
}
/// <summary>
@ -69,30 +51,7 @@ namespace Microsoft.AspNet.Security.Facebook
/// <returns>An <see cref="AuthenticationHandler"/> configured with the <see cref="FacebookAuthenticationOptions"/> supplied to the constructor.</returns>
protected override AuthenticationHandler<FacebookAuthenticationOptions> CreateHandler()
{
return new FacebookAuthenticationHandler(_httpClient, _logger);
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")]
private static HttpMessageHandler ResolveHttpMessageHandler(FacebookAuthenticationOptions options)
{
HttpMessageHandler handler = options.BackchannelHttpHandler ??
#if ASPNET50
new WebRequestHandler();
// If they provided a validator, apply it or fail.
if (options.BackchannelCertificateValidator != null)
{
// Set the cert validate callback
var webRequestHandler = handler as WebRequestHandler;
if (webRequestHandler == null)
{
throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch);
}
webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate;
}
#else
new WinHttpHandler();
#endif
return handler;
return new FacebookAuthenticationHandler(Backchannel, Logger);
}
}
}

View File

@ -1,108 +1,49 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.Facebook
{
/// <summary>
/// Configuration options for <see cref="FacebookAuthenticationMiddleware"/>.
/// </summary>
public class FacebookAuthenticationOptions : AuthenticationOptions
public class FacebookAuthenticationOptions : OAuthAuthenticationOptions<IFacebookAuthenticationNotifications>
{
/// <summary>
/// Initializes a new <see cref="FacebookAuthenticationOptions"/>.
/// </summary>
[SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters",
MessageId = "Microsoft.AspNet.Security.Facebook.FacebookAuthenticationOptions.set_Caption(System.String)", Justification = "Not localizable.")]
public FacebookAuthenticationOptions()
: base(FacebookAuthenticationDefaults.AuthenticationType)
{
Caption = FacebookAuthenticationDefaults.AuthenticationType;
CallbackPath = new PathString("/signin-facebook");
AuthenticationMode = AuthenticationMode.Passive;
Scope = new List<string>();
BackchannelTimeout = TimeSpan.FromSeconds(60);
SendAppSecretProof = true;
AuthorizationEndpoint = FacebookAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = FacebookAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = FacebookAuthenticationDefaults.UserInformationEndpoint;
}
// Facebook uses a non-standard term for this field.
/// <summary>
/// Gets or sets the Facebook-assigned appId.
/// </summary>
public string AppId { get; set; }
public string AppId
{
get { return ClientId; }
set { ClientId = value; }
}
// Facebook uses a non-standard term for this field.
/// <summary>
/// Gets or sets the Facebook-assigned app secret.
/// </summary>
public string AppSecret { get; set; }
#if ASPNET50
/// <summary>
/// Gets or sets the a pinned certificate validator to use to validate the endpoints used
/// in back channel communications belong to Facebook.
/// </summary>
/// <value>
/// The pinned certificate validator.
/// </value>
/// <remarks>If this property is null then the default certificate checks are performed,
/// validating the subject name and if the signing chain is a trusted party.</remarks>
public ICertificateValidator BackchannelCertificateValidator { get; set; }
#endif
/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with Facebook.
/// </summary>
/// <value>
/// The back channel timeout in milliseconds.
/// </value>
public TimeSpan BackchannelTimeout { get; set; }
/// <summary>
/// The HttpMessageHandler used to communicate with Facebook.
/// This cannot be set at the same time as BackchannelCertificateValidator unless the value
/// can be downcast to a WebRequestHandler.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
public string Caption
public string AppSecret
{
get { return Description.Caption; }
set { Description.Caption = value; }
get { return ClientSecret; }
set { ClientSecret = value; }
}
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// Default value is "/signin-facebook".
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public string SignInAsAuthenticationType { get; set; }
/// <summary>
/// Gets or sets the <see cref="IFacebookAuthenticationNotifications"/> used to handle authentication events.
/// </summary>
public IFacebookAuthenticationNotifications Notifications { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>
public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
/// <summary>
/// A list of permissions to request.
/// </summary>
public IList<string> Scope { get; private set; }
/// <summary>
/// Gets or sets if the appsecret_proof should be generated and sent with Facebook API calls.
/// This is enabled by default.

View File

@ -1,43 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Notifications;
namespace Microsoft.AspNet.Security.Facebook
{
/// <summary>
/// The Context passed when a Challenge causes a redirect to authorize endpoint in the Facebook middleware.
/// </summary>
public class FacebookApplyRedirectContext : BaseContext<FacebookAuthenticationOptions>
{
/// <summary>
/// Creates a new context object.
/// </summary>
/// <param name="context">The http request context.</param>
/// <param name="options">The Facebook middleware options.</param>
/// <param name="properties">The authentication properties of the challenge.</param>
/// <param name="redirectUri">The initial redirect URI.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "3#",
Justification = "Represents header value")]
public FacebookApplyRedirectContext(HttpContext context, FacebookAuthenticationOptions options,
AuthenticationProperties properties, string redirectUri)
: base(context, options)
{
RedirectUri = redirectUri;
Properties = properties;
}
/// <summary>
/// Gets the URI used for the redirect operation.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Represents header value")]
public string RedirectUri { get; private set; }
/// <summary>
/// Gets the authentication properties of the challenge.
/// </summary>
public AuthenticationProperties Properties { get; private set; }
}
}

View File

@ -1,12 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Security.Claims;
using System.Net.Http;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Notifications;
using Microsoft.AspNet.Security.OAuth;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.Facebook
@ -14,27 +11,17 @@ namespace Microsoft.AspNet.Security.Facebook
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class FacebookAuthenticatedContext : BaseContext
public class FacebookAuthenticatedContext : OAuthAuthenticatedContext
{
/// <summary>
/// Initializes a new <see cref="FacebookAuthenticatedContext"/>.
/// </summary>
/// <param name="context">The HTTP environment.</param>
/// <param name="user">The JSON-serialized user.</param>
/// <param name="accessToken">The Facebook Access token.</param>
/// <param name="expires">Seconds until expiration.</param>
public FacebookAuthenticatedContext(HttpContext context, JObject user, string accessToken, string expires)
: base(context)
/// <param name="tokens">The Facebook Access token.</param>
public FacebookAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user, TokenResponse tokens)
: base(context, options, user, tokens)
{
User = user;
AccessToken = accessToken;
int expiresValue;
if (Int32.TryParse(expires, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresValue))
{
ExpiresIn = TimeSpan.FromSeconds(expiresValue);
}
Id = TryGetValue(user, "id");
Name = TryGetValue(user, "name");
Link = TryGetValue(user, "link");
@ -42,21 +29,6 @@ namespace Microsoft.AspNet.Security.Facebook
Email = TryGetValue(user, "email");
}
/// <summary>
/// Gets the JSON-serialized user.
/// </summary>
public JObject User { get; private set; }
/// <summary>
/// Gets the Facebook access token.
/// </summary>
public string AccessToken { get; private set; }
/// <summary>
/// Gets the Facebook access token expiration time.
/// </summary>
public TimeSpan? ExpiresIn { get; set; }
/// <summary>
/// Gets the Facebook user ID.
/// </summary>
@ -82,16 +54,6 @@ namespace Microsoft.AspNet.Security.Facebook
/// </summary>
public string Email { get; private set; }
/// <summary>
/// Gets the <see cref="ClaimsIdentity"/> representing the user.
/// </summary>
public ClaimsIdentity Identity { get; set; }
/// <summary>
/// Gets or sets a property bag for common authentication properties.
/// </summary>
public AuthenticationProperties Properties { get; set; }
private static string TryGetValue(JObject user, string propertyName)
{
JToken value;

View File

@ -3,13 +3,14 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.Facebook
{
/// <summary>
/// The default <see cref="IFacebookAuthenticationNotifications"/> implementation.
/// </summary>
public class FacebookAuthenticationNotifications : IFacebookAuthenticationNotifications
public class FacebookAuthenticationNotifications : OAuthAuthenticationNotifications, IFacebookAuthenticationNotifications
{
/// <summary>
/// Initializes a new <see cref="FacebookAuthenticationNotifications"/>.
@ -17,9 +18,6 @@ namespace Microsoft.AspNet.Security.Facebook
public FacebookAuthenticationNotifications()
{
OnAuthenticated = context => Task.FromResult<object>(null);
OnReturnEndpoint = context => Task.FromResult<object>(null);
OnApplyRedirect = context =>
context.Response.Redirect(context.RedirectUri);
}
/// <summary>
@ -27,16 +25,6 @@ namespace Microsoft.AspNet.Security.Facebook
/// </summary>
public Func<FacebookAuthenticatedContext, Task> OnAuthenticated { get; set; }
/// <summary>
/// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked.
/// </summary>
public Func<FacebookReturnEndpointContext, Task> OnReturnEndpoint { get; set; }
/// <summary>
/// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked.
/// </summary>
public Action<FacebookApplyRedirectContext> OnApplyRedirect { get; set; }
/// <summary>
/// Invoked whenever Facebook succesfully authenticates a user.
/// </summary>
@ -46,24 +34,5 @@ namespace Microsoft.AspNet.Security.Facebook
{
return OnAuthenticated(context);
}
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task ReturnEndpoint(FacebookReturnEndpointContext context)
{
return OnReturnEndpoint(context);
}
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
public virtual void ApplyRedirect(FacebookApplyRedirectContext context)
{
OnApplyRedirect(context);
}
}
}

View File

@ -1,26 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Security.Notifications;
namespace Microsoft.AspNet.Security.Facebook
{
/// <summary>
/// Provides context information for notifications.
/// </summary>
public class FacebookReturnEndpointContext : ReturnEndpointContext
{
/// <summary>
/// Creates a new context object.
/// </summary>
/// <param name="context">The http environment.</param>
/// <param name="ticket">The authentication ticket.</param>
public FacebookReturnEndpointContext(
HttpContext context,
AuthenticationTicket ticket)
: base(context, ticket)
{
}
}
}

View File

@ -2,13 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.Facebook
{
/// <summary>
/// Specifies callback methods which the <see cref="FacebookAuthenticationMiddleware"></see> invokes to enable developer control over the authentication process.
/// </summary>
public interface IFacebookAuthenticationNotifications
public interface IFacebookAuthenticationNotifications : IOAuthAuthenticationNotifications
{
/// <summary>
/// Invoked when Facebook succesfully authenticates a user.
@ -16,18 +17,5 @@ namespace Microsoft.AspNet.Security.Facebook
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task Authenticated(FacebookAuthenticatedContext context);
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task ReturnEndpoint(FacebookReturnEndpointContext context);
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
void ApplyRedirect(FacebookApplyRedirectContext context);
}
}

View File

@ -68,14 +68,5 @@ namespace Microsoft.AspNet.Security.Facebook {
return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler..
/// </summary>
internal static string Exception_ValidatorHandlerMismatch {
get {
return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture);
}
}
}
}

View File

@ -120,7 +120,4 @@
<data name="Exception_OptionMustBeProvided" xml:space="preserve">
<value>The '{0}' option must be provided.</value>
</data>
<data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
<value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
</data>
</root>

View File

@ -5,6 +5,7 @@
"Microsoft.AspNet.RequestContainer": "1.0.0-*",
"Microsoft.AspNet.Security": "1.0.0-*",
"Microsoft.AspNet.Security.DataProtection": "1.0.0-*",
"Microsoft.AspNet.Security.OAuth": "1.0.0-*",
"Microsoft.AspNet.WebUtilities": "1.0.0-*",
"Microsoft.Framework.Logging": "1.0.0-*",
"Newtonsoft.Json": "5.0.8",
@ -13,7 +14,6 @@
"frameworks": {
"aspnet50": {
"dependencies": {
"System.Net.Http.WebRequest": ""
}
},
"aspnetcore50": {
@ -27,7 +27,6 @@
"System.IO": "4.0.10.0",
"System.IO.Compression": "4.0.0.0",
"System.Linq": "4.0.0.0",
"System.Net.Http.WinHttpHandler": "4.0.0.0",
"System.Reflection": "4.0.10.0",
"System.Resources.ResourceManager": "4.0.0.0",
"System.Runtime": "4.0.20.0",

View File

@ -6,5 +6,11 @@ namespace Microsoft.AspNet.Security.Google
public static class GoogleAuthenticationDefaults
{
public const string AuthenticationType = "Google";
public const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth";
public const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";
public const string UserInformationEndpoint = "https://www.googleapis.com/plus/v1/people/me";
}
}

View File

@ -7,212 +7,84 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Infrastructure;
using Microsoft.AspNet.Security.OAuth;
using Microsoft.AspNet.WebUtilities;
using Microsoft.Framework.Logging;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.Google
{
internal class GoogleAuthenticationHandler : AuthenticationHandler<GoogleAuthenticationOptions>
internal class GoogleAuthenticationHandler : OAuthAuthenticationHandler<GoogleAuthenticationOptions, IGoogleAuthenticationNotifications>
{
private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";
private const string UserInfoEndpoint = "https://www.googleapis.com/plus/v1/people/me";
private const string AuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth";
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
public GoogleAuthenticationHandler(HttpClient httpClient, ILogger logger)
: base(httpClient, logger)
{
_httpClient = httpClient;
_logger = logger;
}
protected override AuthenticationTicket AuthenticateCore()
protected override async Task<AuthenticationTicket> GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens)
{
return AuthenticateCoreAsync().Result;
// Get the Google user
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
HttpResponseMessage graphResponse = await Backchannel.SendAsync(request, Context.RequestAborted);
graphResponse.EnsureSuccessStatusCode();
var text = await graphResponse.Content.ReadAsStringAsync();
JObject user = JObject.Parse(text);
var context = new GoogleAuthenticatedContext(Context, Options, user, tokens);
context.Identity = new ClaimsIdentity(
Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if (!string.IsNullOrEmpty(context.Id))
{
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.GivenName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.FamilyName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Name))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String,
Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Email))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String,
Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Profile))
{
context.Identity.AddClaim(new Claim("urn:google:profile", context.Profile, ClaimValueTypes.String,
Options.AuthenticationType));
}
context.Properties = properties;
await Options.Notifications.Authenticated(context);
return new AuthenticationTicket(context.Identity, context.Properties);
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
// TODO: Abstract this properties override pattern into the base class?
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
AuthenticationProperties properties = null;
try
{
string code = null;
string state = null;
IReadableStringCollection query = Request.Query;
IList<string> values = query.GetValues("code");
if (values != null && values.Count == 1)
{
code = values[0];
}
values = query.GetValues("state");
if (values != null && values.Count == 1)
{
state = values[0];
}
properties = Options.StateDataFormat.Unprotect(state);
if (properties == null)
{
return null;
}
// OAuth2 10.12 CSRF
if (!ValidateCorrelationId(properties, _logger))
{
return new AuthenticationTicket(null, properties);
}
string requestPrefix = Request.Scheme + "://" + Request.Host;
string redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath;
// Build up the body for the token request
var body = new List<KeyValuePair<string, string>>();
body.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
body.Add(new KeyValuePair<string, string>("code", code));
body.Add(new KeyValuePair<string, string>("redirect_uri", redirectUri));
body.Add(new KeyValuePair<string, string>("client_id", Options.ClientId));
body.Add(new KeyValuePair<string, string>("client_secret", Options.ClientSecret));
// Request the token
HttpResponseMessage tokenResponse =
await _httpClient.PostAsync(TokenEndpoint, new FormUrlEncodedContent(body));
tokenResponse.EnsureSuccessStatusCode();
string text = await tokenResponse.Content.ReadAsStringAsync();
// Deserializes the token response
JObject response = JObject.Parse(text);
string accessToken = response.Value<string>("access_token");
string expires = response.Value<string>("expires_in");
string refreshToken = response.Value<string>("refresh_token");
if (string.IsNullOrWhiteSpace(accessToken))
{
_logger.WriteWarning("Access token was not found");
return new AuthenticationTicket(null, properties);
}
// Get the Google user
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, UserInfoEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
HttpResponseMessage graphResponse = await _httpClient.SendAsync(request, Context.RequestAborted);
graphResponse.EnsureSuccessStatusCode();
text = await graphResponse.Content.ReadAsStringAsync();
JObject user = JObject.Parse(text);
var context = new GoogleAuthenticatedContext(Context, user, accessToken, refreshToken, expires);
context.Identity = new ClaimsIdentity(
Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if (!string.IsNullOrEmpty(context.Id))
{
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.GivenName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.FamilyName))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName,
ClaimValueTypes.String, Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Name))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String,
Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Email))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String,
Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.Profile))
{
context.Identity.AddClaim(new Claim("urn:google:profile", context.Profile, ClaimValueTypes.String,
Options.AuthenticationType));
}
context.Properties = properties;
await Options.Notifications.Authenticated(context);
return new AuthenticationTicket(context.Identity, context.Properties);
}
catch (Exception ex)
{
_logger.WriteError("Authentication failed", ex);
return new AuthenticationTicket(null, properties);
}
}
protected override void ApplyResponseChallenge()
{
if (Response.StatusCode != 401)
{
return;
}
// Active middleware should redirect on 401 even if there wasn't an explicit challenge.
if (ChallengeContext == null && Options.AuthenticationMode == AuthenticationMode.Passive)
{
return;
}
string baseUri =
Request.Scheme +
"://" +
Request.Host +
Request.PathBase;
string currentUri =
baseUri +
Request.Path +
Request.QueryString;
string redirectUri =
baseUri +
Options.CallbackPath;
AuthenticationProperties properties;
if (ChallengeContext == null)
{
properties = new AuthenticationProperties();
}
else
{
properties = new AuthenticationProperties(ChallengeContext.Properties);
}
if (string.IsNullOrEmpty(properties.RedirectUri))
{
properties.RedirectUri = currentUri;
}
// OAuth2 10.12 CSRF
GenerateCorrelationId(properties);
string scope = FormatScope();
var queryStrings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
queryStrings.Add("response_type", "code");
queryStrings.Add("client_id", Options.ClientId);
queryStrings.Add("redirect_uri", redirectUri);
// space separated
string scope = string.Join(" ", Options.Scope);
if (string.IsNullOrEmpty(scope))
{
// Google OAuth 2.0 asks for non-empty scope. If user didn't set it, set default scope to
// "openid profile email" to get basic user information.
scope = "openid profile email";
}
AddQueryString(queryStrings, properties, "scope", scope);
AddQueryString(queryStrings, properties, "access_type", Options.AccessType);
@ -222,65 +94,8 @@ namespace Microsoft.AspNet.Security.Google
string state = Options.StateDataFormat.Protect(properties);
queryStrings.Add("state", state);
string authorizationEndpoint = QueryHelpers.AddQueryString(AuthorizeEndpoint, queryStrings);
var redirectContext = new GoogleApplyRedirectContext(
Context, Options,
properties, authorizationEndpoint);
Options.Notifications.ApplyRedirect(redirectContext);
}
public override async Task<bool> InvokeAsync()
{
return await InvokeReplyPathAsync();
}
private async Task<bool> InvokeReplyPathAsync()
{
if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
{
// TODO: error responses
AuthenticationTicket ticket = await AuthenticateAsync();
if (ticket == null)
{
_logger.WriteWarning("Invalid return state, unable to redirect.");
Response.StatusCode = 500;
return true;
}
var context = new GoogleReturnEndpointContext(Context, ticket);
context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType;
context.RedirectUri = ticket.Properties.RedirectUri;
await Options.Notifications.ReturnEndpoint(context);
if (context.SignInAsAuthenticationType != null &&
context.Identity != null)
{
ClaimsIdentity grantIdentity = context.Identity;
if (!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
{
grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType);
}
Context.Response.SignIn(context.Properties, grantIdentity);
}
if (!context.IsRequestCompleted && context.RedirectUri != null)
{
string redirectUri = context.RedirectUri;
if (context.Identity == null)
{
// add a redirect hint that sign-in failed in some way
redirectUri = QueryHelpers.AddQueryString(redirectUri, "error", "access_denied");
}
Response.Redirect(redirectUri);
context.RequestCompleted();
}
return context.IsRequestCompleted;
}
return false;
string authorizationEndpoint = QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, queryStrings);
return authorizationEndpoint;
}
private static void AddQueryString(IDictionary<string, string> queryStrings, AuthenticationProperties properties,
@ -304,10 +119,5 @@ namespace Microsoft.AspNet.Security.Google
queryStrings[name] = value;
}
protected override void ApplyResponseGrant()
{
// N/A - No SignIn or SignOut support.
}
}
}

View File

@ -9,6 +9,7 @@ using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Security.DataHandler;
using Microsoft.AspNet.Security.DataProtection;
using Microsoft.AspNet.Security.Infrastructure;
using Microsoft.AspNet.Security.OAuth;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Security.Google
@ -17,11 +18,8 @@ namespace Microsoft.AspNet.Security.Google
/// An ASP.NET middleware for authenticating users using Google OAuth 2.0.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")]
public class GoogleAuthenticationMiddleware : AuthenticationMiddleware<GoogleAuthenticationOptions>
public class GoogleAuthenticationMiddleware : OAuthAuthenticationMiddleware<GoogleAuthenticationOptions, IGoogleAuthenticationNotifications>
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
/// <summary>
/// Initializes a new <see cref="GoogleAuthenticationMiddleware"/>.
/// </summary>
@ -34,33 +32,22 @@ namespace Microsoft.AspNet.Security.Google
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
GoogleAuthenticationOptions options)
: base(next, options)
: base(next, dataProtectionProvider, loggerFactory, options)
{
if (string.IsNullOrWhiteSpace(Options.ClientId))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientId"));
}
if (string.IsNullOrWhiteSpace(Options.ClientSecret))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientSecret"));
}
_logger = loggerFactory.Create(typeof(GoogleAuthenticationMiddleware).FullName);
if (Options.Notifications == null)
{
Options.Notifications = new GoogleAuthenticationNotifications();
}
if (Options.StateDataFormat == null)
{
IDataProtector dataProtector = DataProtectionHelpers.CreateDataProtector(dataProtectionProvider,
typeof(GoogleAuthenticationMiddleware).FullName, options.AuthenticationType, "v1");
Options.StateDataFormat = new PropertiesDataFormat(dataProtector);
}
_httpClient = new HttpClient(ResolveHttpMessageHandler(Options));
_httpClient.Timeout = Options.BackchannelTimeout;
_httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
if (Options.Scope.Count == 0)
{
// Google OAuth 2.0 asks for non-empty scope. If user didn't set it, set default scope to
// "openid profile email" to get basic user information.
// TODO: Should we just add these by default when we create the Options?
Options.Scope.Add("openid");
Options.Scope.Add("profile");
Options.Scope.Add("email");
}
}
/// <summary>
@ -69,30 +56,7 @@ namespace Microsoft.AspNet.Security.Google
/// <returns>An <see cref="AuthenticationHandler"/> configured with the <see cref="GoogleAuthenticationOptions"/> supplied to the constructor.</returns>
protected override AuthenticationHandler<GoogleAuthenticationOptions> CreateHandler()
{
return new GoogleAuthenticationHandler(_httpClient, _logger);
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")]
private static HttpMessageHandler ResolveHttpMessageHandler(GoogleAuthenticationOptions options)
{
HttpMessageHandler handler = options.BackchannelHttpHandler ??
#if ASPNET50
new WebRequestHandler();
// If they provided a validator, apply it or fail.
if (options.BackchannelCertificateValidator != null)
{
// Set the cert validate callback
var webRequestHandler = handler as WebRequestHandler;
if (webRequestHandler == null)
{
throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch);
}
webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate;
}
#else
new WinHttpHandler();
#endif
return handler;
return new GoogleAuthenticationHandler(Backchannel, Logger);
}
}
}

View File

@ -6,102 +6,27 @@ using System.Collections.Generic;
using System.Net.Http;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.Google
{
/// <summary>
/// Configuration options for <see cref="GoogleAuthenticationMiddleware"/>.
/// </summary>
public class GoogleAuthenticationOptions : AuthenticationOptions
public class GoogleAuthenticationOptions : OAuthAuthenticationOptions<IGoogleAuthenticationNotifications>
{
/// <summary>
/// Initializes a new <see cref="GoogleAuthenticationOptions"/>.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters",
MessageId = "Microsoft.AspNet.Security.Google.GoogleAuthenticationOptions.set_Caption(System.String)",
Justification = "Not localizable.")]
public GoogleAuthenticationOptions()
: base(GoogleAuthenticationDefaults.AuthenticationType)
{
Caption = GoogleAuthenticationDefaults.AuthenticationType;
CallbackPath = new PathString("/signin-google");
AuthenticationMode = AuthenticationMode.Passive;
Scope = new List<string>();
BackchannelTimeout = TimeSpan.FromSeconds(60);
AuthorizationEndpoint = GoogleAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = GoogleAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = GoogleAuthenticationDefaults.UserInformationEndpoint;
}
/// <summary>
/// Gets or sets the Google-assigned client id.
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// Gets or sets the Google-assigned client secret.
/// </summary>
public string ClientSecret { get; set; }
#if ASPNET50
/// <summary>
/// Gets or sets the a pinned certificate validator to use to validate the endpoints used
/// in back channel communications belong to Google.
/// </summary>
/// <value>
/// The pinned certificate validator.
/// </value>
/// <remarks>If this property is null then the default certificate checks are performed,
/// validating the subject name and if the signing chain is a trusted party.</remarks>
public ICertificateValidator BackchannelCertificateValidator { get; set; }
#endif
/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with Google.
/// </summary>
/// <value>
/// The back channel timeout in milliseconds.
/// </value>
public TimeSpan BackchannelTimeout { get; set; }
/// <summary>
/// The HttpMessageHandler used to communicate with Google.
/// This cannot be set at the same time as BackchannelCertificateValidator unless the value
/// can be downcast to a WebRequestHandler.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
public string Caption
{
get { return Description.Caption; }
set { Description.Caption = value; }
}
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// Default value is "/signin-google".
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public string SignInAsAuthenticationType { get; set; }
/// <summary>
/// Gets or sets the <see cref="IGoogleAuthenticationNotifications"/> used to handle authentication events.
/// </summary>
public IGoogleAuthenticationNotifications Notifications { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>
public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
/// <summary>
/// A list of permissions to request.
/// </summary>
public IList<string> Scope { get; private set; }
/// <summary>
/// access_type. Set to 'offline' to request a refresh token.
/// </summary>

View File

@ -1,43 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Notifications;
namespace Microsoft.AspNet.Security.Google
{
/// <summary>
/// The Context passed when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware.
/// </summary>
public class GoogleApplyRedirectContext : BaseContext<GoogleAuthenticationOptions>
{
/// <summary>
/// Creates a new context object.
/// </summary>
/// <param name="context">The HTTP request context.</param>
/// <param name="options">The Google OAuth 2.0 middleware options.</param>
/// <param name="properties">The authentication properties of the challenge.</param>
/// <param name="redirectUri">The initial redirect URI.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "3#",
Justification = "Represents header value")]
public GoogleApplyRedirectContext(HttpContext context, GoogleAuthenticationOptions options,
AuthenticationProperties properties, string redirectUri)
: base(context, options)
{
RedirectUri = redirectUri;
Properties = properties;
}
/// <summary>
/// Gets the URI used for the redirect operation.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Represents header value")]
public string RedirectUri { get; private set; }
/// <summary>
/// Gets the authentication properties of the challenge.
/// </summary>
public AuthenticationProperties Properties { get; private set; }
}
}

View File

@ -3,10 +3,11 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Security.Claims;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Notifications;
using Microsoft.AspNet.Security.OAuth;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.Google
@ -14,30 +15,17 @@ namespace Microsoft.AspNet.Security.Google
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class GoogleAuthenticatedContext : BaseContext
public class GoogleAuthenticatedContext : OAuthAuthenticatedContext
{
/// <summary>
/// Initializes a new <see cref="GoogleAuthenticatedContext"/>.
/// </summary>
/// <param name="context">The HTTP environment.</param>
/// <param name="user">The JSON-serialized Google user info.</param>
/// <param name="accessToken">Google OAuth 2.0 access token.</param>
/// <param name="refreshToken">Goolge OAuth 2.0 refresh token.</param>
/// <param name="expires">Seconds until expiration.</param>
public GoogleAuthenticatedContext(HttpContext context, JObject user, string accessToken,
string refreshToken, string expires)
: base(context)
/// <param name="tokens">Google OAuth 2.0 access token, refresh token, etc.</param>
public GoogleAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user, TokenResponse tokens)
: base(context, options, user, tokens)
{
User = user;
AccessToken = accessToken;
RefreshToken = refreshToken;
int expiresValue;
if (Int32.TryParse(expires, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresValue))
{
ExpiresIn = TimeSpan.FromSeconds(expiresValue);
}
Id = TryGetValue(user, "id");
Name = TryGetValue(user, "displayName");
GivenName = TryGetValue(user, "name", "givenName");
@ -46,32 +34,6 @@ namespace Microsoft.AspNet.Security.Google
Email = TryGetFirstValue(user, "emails", "value");
}
/// <summary>
/// Gets the JSON-serialized user.
/// </summary>
/// <remarks>
/// Contains the Google user obtained from the userinfo endpoint.
/// </remarks>
public JObject User { get; private set; }
/// <summary>
/// Gets the Google access token.
/// </summary>
public string AccessToken { get; private set; }
/// <summary>
/// Gets the Google refresh token.
/// </summary>
/// <remarks>
/// This value is not null only when access_type authorize parameter is offline.
/// </remarks>
public string RefreshToken { get; private set; }
/// <summary>
/// Gets the Google access token expiration time.
/// </summary>
public TimeSpan? ExpiresIn { get; set; }
/// <summary>
/// Gets the Google user ID.
/// </summary>
@ -102,16 +64,6 @@ namespace Microsoft.AspNet.Security.Google
/// </summary>
public string Email { get; private set; }
/// <summary>
/// Gets the <see cref="ClaimsIdentity"/> representing the user.
/// </summary>
public ClaimsIdentity Identity { get; set; }
/// <summary>
/// Gets or sets a property bag for common authentication properties.
/// </summary>
public AuthenticationProperties Properties { get; set; }
private static string TryGetValue(JObject user, string propertyName)
{
JToken value;

View File

@ -3,13 +3,14 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.Google
{
/// <summary>
/// The default <see cref="IGoogleAuthenticationNotifications"/> implementation.
/// </summary>
public class GoogleAuthenticationNotifications : IGoogleAuthenticationNotifications
public class GoogleAuthenticationNotifications : OAuthAuthenticationNotifications, IGoogleAuthenticationNotifications
{
/// <summary>
/// Initializes a new <see cref="GoogleAuthenticationNotifications"/>.
@ -17,8 +18,6 @@ namespace Microsoft.AspNet.Security.Google
public GoogleAuthenticationNotifications()
{
OnAuthenticated = context => Task.FromResult<object>(null);
OnReturnEndpoint = context => Task.FromResult<object>(null);
OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri);
}
/// <summary>
@ -26,16 +25,6 @@ namespace Microsoft.AspNet.Security.Google
/// </summary>
public Func<GoogleAuthenticatedContext, Task> OnAuthenticated { get; set; }
/// <summary>
/// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked.
/// </summary>
public Func<GoogleReturnEndpointContext, Task> OnReturnEndpoint { get; set; }
/// <summary>
/// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked.
/// </summary>
public Action<GoogleApplyRedirectContext> OnApplyRedirect { get; set; }
/// <summary>
/// Invoked whenever Google succesfully authenticates a user.
/// </summary>
@ -45,24 +34,5 @@ namespace Microsoft.AspNet.Security.Google
{
return OnAuthenticated(context);
}
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context">Contains context information and authentication ticket of the return endpoint.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task ReturnEndpoint(GoogleReturnEndpointContext context)
{
return OnReturnEndpoint(context);
}
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
public virtual void ApplyRedirect(GoogleApplyRedirectContext context)
{
OnApplyRedirect(context);
}
}
}

View File

@ -1,26 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Security.Notifications;
namespace Microsoft.AspNet.Security.Google
{
/// <summary>
/// Provides context information to middleware notifications.
/// </summary>
public class GoogleReturnEndpointContext : ReturnEndpointContext
{
/// <summary>
/// Initialize a <see cref="GoogleReturnEndpointContext"/>.
/// </summary>
/// <param name="context">The HTTP environment.</param>
/// <param name="ticket">The authentication ticket.</param>
public GoogleReturnEndpointContext(
HttpContext context,
AuthenticationTicket ticket)
: base(context, ticket)
{
}
}
}

View File

@ -2,13 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.Google
{
/// <summary>
/// Specifies callback methods which the <see cref="GoogleAuthenticationMiddleware" /> invokes to enable developer control over the authentication process.
/// </summary>
public interface IGoogleAuthenticationNotifications
public interface IGoogleAuthenticationNotifications : IOAuthAuthenticationNotifications
{
/// <summary>
/// Invoked whenever Google succesfully authenticates a user.
@ -16,18 +17,5 @@ namespace Microsoft.AspNet.Security.Google
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task Authenticated(GoogleAuthenticatedContext context);
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context">Contains context information and authentication ticket of the return endpoint.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task ReturnEndpoint(GoogleReturnEndpointContext context);
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
void ApplyRedirect(GoogleApplyRedirectContext context);
}
}

View File

@ -5,6 +5,7 @@
"Microsoft.AspNet.RequestContainer": "1.0.0-*",
"Microsoft.AspNet.Security": "1.0.0-*",
"Microsoft.AspNet.Security.DataProtection": "1.0.0-*",
"Microsoft.AspNet.Security.OAuth": "1.0.0-*",
"Microsoft.AspNet.WebUtilities": "1.0.0-*",
"Microsoft.Framework.Logging": "1.0.0-*",
"Newtonsoft.Json": "5.0.8",
@ -13,7 +14,6 @@
"frameworks": {
"aspnet50": {
"dependencies": {
"System.Net.Http.WebRequest": ""
}
},
"aspnetcore50": {
@ -27,7 +27,6 @@
"System.IO": "4.0.10.0",
"System.IO.Compression": "4.0.0.0",
"System.Linq": "4.0.0.0",
"System.Net.Http.WinHttpHandler": "4.0.0.0",
"System.Reflection": "4.0.10.0",
"System.Resources.ResourceManager": "4.0.0.0",
"System.Runtime": "4.0.20.0",

View File

@ -6,5 +6,11 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
public static class MicrosoftAccountAuthenticationDefaults
{
public const string AuthenticationType = "Microsoft";
public const string AuthorizationEndpoint = "https://login.live.com/oauth20_authorize.srf";
public const string TokenEndpoint = "https://login.live.com/oauth20_token.srf";
public const string UserInformationEndpoint = "https://apis.live.net/v5.0/me";
}
}

View File

@ -4,254 +4,55 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Infrastructure;
using Microsoft.AspNet.WebUtilities;
using Microsoft.AspNet.Security.OAuth;
using Microsoft.Framework.Logging;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.MicrosoftAccount
{
internal class MicrosoftAccountAuthenticationHandler : AuthenticationHandler<MicrosoftAccountAuthenticationOptions>
internal class MicrosoftAccountAuthenticationHandler : OAuthAuthenticationHandler<MicrosoftAccountAuthenticationOptions, IMicrosoftAccountAuthenticationNotifications>
{
private const string TokenEndpoint = "https://login.live.com/oauth20_token.srf";
private const string GraphApiEndpoint = "https://apis.live.net/v5.0/me";
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
public MicrosoftAccountAuthenticationHandler(HttpClient httpClient, ILogger logger)
: base(httpClient, logger)
{
_httpClient = httpClient;
_logger = logger;
}
public override async Task<bool> InvokeAsync()
protected override async Task<AuthenticationTicket> GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens)
{
if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
{
return await InvokeReturnPathAsync();
}
return false;
}
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
HttpResponseMessage graphResponse = await Backchannel.SendAsync(request, Context.RequestAborted);
graphResponse.EnsureSuccessStatusCode();
string accountString = await graphResponse.Content.ReadAsStringAsync();
JObject accountInformation = JObject.Parse(accountString);
protected override AuthenticationTicket AuthenticateCore()
{
return AuthenticateCoreAsync().Result;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
AuthenticationProperties properties = null;
try
{
string code = null;
string state = null;
IReadableStringCollection query = Request.Query;
IList<string> values = query.GetValues("code");
if (values != null && values.Count == 1)
{
code = values[0];
}
values = query.GetValues("state");
if (values != null && values.Count == 1)
{
state = values[0];
}
properties = Options.StateDataFormat.Unprotect(state);
if (properties == null)
{
return null;
}
// OAuth2 10.12 CSRF
if (!ValidateCorrelationId(properties, _logger))
{
return new AuthenticationTicket(null, properties);
}
var tokenRequestParameters = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("client_id", Options.ClientId),
new KeyValuePair<string, string>("redirect_uri", GenerateRedirectUri()),
new KeyValuePair<string, string>("client_secret", Options.ClientSecret),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
};
var requestContent = new FormUrlEncodedContent(tokenRequestParameters);
HttpResponseMessage response = await _httpClient.PostAsync(TokenEndpoint, requestContent, Context.RequestAborted);
response.EnsureSuccessStatusCode();
string oauthTokenResponse = await response.Content.ReadAsStringAsync();
JObject oauth2Token = JObject.Parse(oauthTokenResponse);
var accessToken = oauth2Token["access_token"].Value<string>();
// Refresh token is only available when wl.offline_access is request.
// Otherwise, it is null.
var refreshToken = oauth2Token.Value<string>("refresh_token");
var expire = oauth2Token.Value<string>("expires_in");
if (string.IsNullOrWhiteSpace(accessToken))
{
_logger.WriteWarning("Access token was not found");
return new AuthenticationTicket(null, properties);
}
HttpResponseMessage graphResponse = await _httpClient.GetAsync(
GraphApiEndpoint + "?access_token=" + Uri.EscapeDataString(accessToken), Context.RequestAborted);
graphResponse.EnsureSuccessStatusCode();
string accountString = await graphResponse.Content.ReadAsStringAsync();
JObject accountInformation = JObject.Parse(accountString);
var context = new MicrosoftAccountAuthenticatedContext(Context, accountInformation, accessToken,
refreshToken, expire);
context.Identity = new ClaimsIdentity(
new[]
var context = new MicrosoftAccountAuthenticatedContext(Context, Options, accountInformation, tokens);
context.Properties = properties;
context.Identity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.NameIdentifier, context.Id, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType),
new Claim(ClaimTypes.Name, context.Name, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType),
new Claim("urn:microsoftaccount:id", context.Id, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType),
new Claim("urn:microsoftaccount:name", context.Name, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType)
new Claim(ClaimTypes.NameIdentifier, context.Id, ClaimValueTypes.String, Options.AuthenticationType),
new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String, Options.AuthenticationType),
new Claim("urn:microsoftaccount:id", context.Id, ClaimValueTypes.String, Options.AuthenticationType),
new Claim("urn:microsoftaccount:name", context.Name, ClaimValueTypes.String, Options.AuthenticationType)
},
Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if (!string.IsNullOrWhiteSpace(context.Email))
{
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType));
}
Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
await Options.Notifications.Authenticated(context);
context.Properties = properties;
return new AuthenticationTicket(context.Identity, context.Properties);
}
catch (Exception ex)
if (!string.IsNullOrWhiteSpace(context.Email))
{
_logger.WriteError("Authentication failed", ex);
return new AuthenticationTicket(null, properties);
}
}
protected override void ApplyResponseChallenge()
{
if (Response.StatusCode != 401)
{
return;
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, Options.AuthenticationType));
}
// Active middleware should redirect on 401 even if there wasn't an explicit challenge.
if (ChallengeContext == null && Options.AuthenticationMode == AuthenticationMode.Passive)
{
return;
}
await Options.Notifications.Authenticated(context);
string baseUri = Request.Scheme + "://" + Request.Host + Request.PathBase;
string currentUri = baseUri + Request.Path + Request.QueryString;
string redirectUri = baseUri + Options.CallbackPath;
AuthenticationProperties properties;
if (ChallengeContext == null)
{
properties = new AuthenticationProperties();
}
else
{
properties = new AuthenticationProperties(ChallengeContext.Properties);
}
if (string.IsNullOrEmpty(properties.RedirectUri))
{
properties.RedirectUri = currentUri;
}
// OAuth2 10.12 CSRF
GenerateCorrelationId(properties);
// OAuth2 3.3 space separated
string scope = string.Join(" ", Options.Scope);
// LiveID requires a scope string, so if the user didn't set one we go for the least possible.
if (string.IsNullOrWhiteSpace(scope))
{
scope = "wl.basic";
}
string state = Options.StateDataFormat.Protect(properties);
string authorizationEndpoint =
"https://login.live.com/oauth20_authorize.srf" +
"?client_id=" + Uri.EscapeDataString(Options.ClientId) +
"&scope=" + Uri.EscapeDataString(scope) +
"&response_type=code" +
"&redirect_uri=" + Uri.EscapeDataString(redirectUri) +
"&state=" + Uri.EscapeDataString(state);
var redirectContext = new MicrosoftAccountApplyRedirectContext(
Context, Options,
properties, authorizationEndpoint);
Options.Notifications.ApplyRedirect(redirectContext);
}
public async Task<bool> InvokeReturnPathAsync()
{
AuthenticationTicket model = await AuthenticateAsync();
if (model == null)
{
_logger.WriteWarning("Invalid return state, unable to redirect.");
Response.StatusCode = 500;
return true;
}
var context = new MicrosoftAccountReturnEndpointContext(Context, model);
context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType;
context.RedirectUri = model.Properties.RedirectUri;
model.Properties.RedirectUri = null;
await Options.Notifications.ReturnEndpoint(context);
if (context.SignInAsAuthenticationType != null && context.Identity != null)
{
ClaimsIdentity signInIdentity = context.Identity;
if (!string.Equals(signInIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
{
signInIdentity = new ClaimsIdentity(signInIdentity.Claims, context.SignInAsAuthenticationType, signInIdentity.NameClaimType, signInIdentity.RoleClaimType);
}
Context.Response.SignIn(context.Properties, signInIdentity);
}
if (!context.IsRequestCompleted && context.RedirectUri != null)
{
if (context.Identity == null)
{
// add a redirect hint that sign-in failed in some way
context.RedirectUri = QueryHelpers.AddQueryString(context.RedirectUri, "error", "access_denied");
}
Response.Redirect(context.RedirectUri);
context.RequestCompleted();
}
return context.IsRequestCompleted;
}
private string GenerateRedirectUri()
{
string requestPrefix = Request.Scheme + "://" + Request.Host;
return requestPrefix + RequestPathBase + Options.CallbackPath;
}
protected override void ApplyResponseGrant()
{
// N/A - No SignIn or SignOut support.
return new AuthenticationTicket(context.Identity, context.Properties);
}
}
}

View File

@ -2,13 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Security.DataHandler;
using Microsoft.AspNet.Security.DataProtection;
using Microsoft.AspNet.Security.Infrastructure;
using Microsoft.AspNet.Security.OAuth;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Security.MicrosoftAccount
@ -16,12 +14,8 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
/// <summary>
/// An ASP.NET middleware for authenticating users using the Microsoft Account service.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")]
public class MicrosoftAccountAuthenticationMiddleware : AuthenticationMiddleware<MicrosoftAccountAuthenticationOptions>
public class MicrosoftAccountAuthenticationMiddleware : OAuthAuthenticationMiddleware<MicrosoftAccountAuthenticationOptions, IMicrosoftAccountAuthenticationNotifications>
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
/// <summary>
/// Initializes a new <see cref="MicrosoftAccountAuthenticationMiddleware"/>.
/// </summary>
@ -34,33 +28,18 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
MicrosoftAccountAuthenticationOptions options)
: base(next, options)
: base(next, dataProtectionProvider, loggerFactory, options)
{
if (string.IsNullOrWhiteSpace(Options.ClientId))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientId"));
}
if (string.IsNullOrWhiteSpace(Options.ClientSecret))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientSecret"));
}
_logger = loggerFactory.Create(typeof(MicrosoftAccountAuthenticationMiddleware).FullName);
if (Options.Notifications == null)
{
Options.Notifications = new MicrosoftAccountAuthenticationNotifications();
}
if (Options.StateDataFormat == null)
if (Options.Scope.Count == 0)
{
IDataProtector dataProtector = DataProtectionHelpers.CreateDataProtector(dataProtectionProvider,
typeof(MicrosoftAccountAuthenticationMiddleware).FullName, options.AuthenticationType, "v1");
Options.StateDataFormat = new PropertiesDataFormat(dataProtector);
// LiveID requires a scope string, so if the user didn't set one we go for the least possible.
// TODO: Should we just add these by default when we create the Options?
Options.Scope.Add("wl.basic");
}
_httpClient = new HttpClient(ResolveHttpMessageHandler(Options));
_httpClient.Timeout = Options.BackchannelTimeout;
_httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
}
/// <summary>
@ -69,30 +48,7 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
/// <returns>An <see cref="AuthenticationHandler"/> configured with the <see cref="MicrosoftAccountAuthenticationOptions"/> supplied to the constructor.</returns>
protected override AuthenticationHandler<MicrosoftAccountAuthenticationOptions> CreateHandler()
{
return new MicrosoftAccountAuthenticationHandler(_httpClient, _logger);
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")]
private static HttpMessageHandler ResolveHttpMessageHandler(MicrosoftAccountAuthenticationOptions options)
{
HttpMessageHandler handler = options.BackchannelHttpHandler ??
#if ASPNET50
new WebRequestHandler();
// If they provided a validator, apply it or fail.
if (options.BackchannelCertificateValidator != null)
{
// Set the cert validate callback
var webRequestHandler = handler as WebRequestHandler;
if (webRequestHandler == null)
{
throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch);
}
webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate;
}
#else
new WinHttpHandler();
#endif
return handler;
return new MicrosoftAccountAuthenticationHandler(Backchannel, Logger);
}
}
}

View File

@ -1,19 +1,15 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.MicrosoftAccount
{
/// <summary>
/// Configuration options for <see cref="MicrosoftAccountAuthenticationMiddleware"/>.
/// </summary>
public class MicrosoftAccountAuthenticationOptions : AuthenticationOptions
public class MicrosoftAccountAuthenticationOptions : OAuthAuthenticationOptions<IMicrosoftAccountAuthenticationNotifications>
{
/// <summary>
/// Initializes a new <see cref="MicrosoftAccountAuthenticationOptions"/>.
@ -21,86 +17,10 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
public MicrosoftAccountAuthenticationOptions()
: base(MicrosoftAccountAuthenticationDefaults.AuthenticationType)
{
Caption = MicrosoftAccountAuthenticationDefaults.AuthenticationType;
CallbackPath = new PathString("/signin-microsoft");
AuthenticationMode = AuthenticationMode.Passive;
Scope = new List<string>();
BackchannelTimeout = TimeSpan.FromSeconds(60);
AuthorizationEndpoint = MicrosoftAccountAuthenticationDefaults.AuthorizationEndpoint;
TokenEndpoint = MicrosoftAccountAuthenticationDefaults.TokenEndpoint;
UserInformationEndpoint = MicrosoftAccountAuthenticationDefaults.UserInformationEndpoint;
}
#if ASPNET50
/// <summary>
/// Gets or sets the a pinned certificate validator to use to validate the endpoints used
/// in back channel communications belong to Microsoft Account.
/// </summary>
/// <value>
/// The pinned certificate validator.
/// </value>
/// <remarks>If this property is null then the default certificate checks are performed,
/// validating the subject name and if the signing chain is a trusted party.</remarks>
public ICertificateValidator BackchannelCertificateValidator { get; set; }
#endif
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
/// <remarks>
/// The default value is 'Microsoft'.
/// </remarks>
public string Caption
{
get { return Description.Caption; }
set { Description.Caption = value; }
}
/// <summary>
/// The application client ID assigned by the Microsoft authentication service.
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// The application client secret assigned by the Microsoft authentication service.
/// </summary>
public string ClientSecret { get; set; }
/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with Microsoft.
/// </summary>
/// <value>
/// The back channel timeout.
/// </value>
public TimeSpan BackchannelTimeout { get; set; }
/// <summary>
/// The HttpMessageHandler used to communicate with Microsoft.
/// This cannot be set at the same time as BackchannelCertificateValidator unless the value
/// can be downcast to a WebRequestHandler.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// A list of permissions to request.
/// </summary>
public IList<string> Scope { get; private set; }
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// Default value is "/signin-microsoft".
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public string SignInAsAuthenticationType { get; set; }
/// <summary>
/// Gets or sets the <see cref="IMicrosoftAccountAuthenticationNotifications"/> used to handle authentication events.
/// </summary>
public IMicrosoftAccountAuthenticationNotifications Notifications { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>
public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
}
}

View File

@ -2,13 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.MicrosoftAccount
{
/// <summary>
/// Specifies callback methods which the <see cref="MicrosoftAccountAuthenticationMiddleware"/> invokes to enable developer control over the authentication process.
/// </summary>
public interface IMicrosoftAccountAuthenticationNotifications
public interface IMicrosoftAccountAuthenticationNotifications : IOAuthAuthenticationNotifications
{
/// <summary>
/// Invoked whenever Microsoft succesfully authenticates a user.
@ -16,18 +17,5 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task Authenticated(MicrosoftAccountAuthenticatedContext context);
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task ReturnEndpoint(MicrosoftAccountReturnEndpointContext context);
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
void ApplyRedirect(MicrosoftAccountApplyRedirectContext context);
}
}

View File

@ -3,12 +3,10 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Net.Http;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Notifications;
using Microsoft.AspNet.Security.OAuth;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.MicrosoftAccount
@ -16,32 +14,19 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class MicrosoftAccountAuthenticatedContext : BaseContext
public class MicrosoftAccountAuthenticatedContext : OAuthAuthenticatedContext
{
/// <summary>
/// Initializes a new <see cref="MicrosoftAccountAuthenticatedContext"/>.
/// </summary>
/// <param name="context">The HTTP environment.</param>
/// <param name="user">The JSON-serialized user.</param>
/// <param name="accessToken">The access token provided by the Microsoft authentication service.</param>
/// <param name="refreshToken">The refresh token provided by Microsoft authentication service.</param>
/// <param name="expires">Seconds until expiration.</param>
public MicrosoftAccountAuthenticatedContext(HttpContext context, [NotNull] JObject user, string accessToken,
string refreshToken, string expires)
: base(context)
/// <param name="tokens">The access token provided by the Microsoft authentication service.</param>
public MicrosoftAccountAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, [NotNull] JObject user, TokenResponse tokens)
: base(context, options, user, tokens)
{
IDictionary<string, JToken> userAsDictionary = user;
User = user;
AccessToken = accessToken;
RefreshToken = refreshToken;
int expiresValue;
if (Int32.TryParse(expires, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresValue))
{
ExpiresIn = TimeSpan.FromSeconds(expiresValue);
}
JToken userId = User["id"];
if (userId == null)
{
@ -63,30 +48,6 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
}
}
/// <summary>
/// Gets the JSON-serialized user.
/// </summary>
public JObject User { get; private set; }
/// <summary>
/// Gets the access token provided by the Microsoft authenication service.
/// </summary>
public string AccessToken { get; private set; }
/// <summary>
/// Gets the refresh token provided by Microsoft authentication service.
/// </summary>
/// <remarks>
/// Refresh token is only available when wl.offline_access is request.
/// Otherwise, it is null.
/// </remarks>
public string RefreshToken { get; private set; }
/// <summary>
/// Gets the Microsoft access token expiration time.
/// </summary>
public TimeSpan? ExpiresIn { get; set; }
/// <summary>
/// Gets the Microsoft Account user ID.
/// </summary>
@ -112,16 +73,6 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
/// </summary>
public string Email { get; private set; }
/// <summary>
/// Gets the <see cref="ClaimsIdentity"/> representing the user.
/// </summary>
public ClaimsIdentity Identity { get; set; }
/// <summary>
/// Gets or sets a property bag for common authentication properties.
/// </summary>
public AuthenticationProperties Properties { get; set; }
private static string PropertyValueIfExists(string property, IDictionary<string, JToken> dictionary)
{
return dictionary.ContainsKey(property) ? dictionary[property].ToString() : null;

View File

@ -3,13 +3,14 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Security.MicrosoftAccount
{
/// <summary>
/// Default <see cref="IMicrosoftAccountAuthenticationNotifications"/> implementation.
/// </summary>
public class MicrosoftAccountAuthenticationNotifications : IMicrosoftAccountAuthenticationNotifications
public class MicrosoftAccountAuthenticationNotifications : OAuthAuthenticationNotifications, IMicrosoftAccountAuthenticationNotifications
{
/// <summary>
/// Initializes a new <see cref="MicrosoftAccountAuthenticationNotifications"/>
@ -17,8 +18,6 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
public MicrosoftAccountAuthenticationNotifications()
{
OnAuthenticated = context => Task.FromResult(0);
OnReturnEndpoint = context => Task.FromResult(0);
OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri);
}
/// <summary>
@ -26,16 +25,6 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
/// </summary>
public Func<MicrosoftAccountAuthenticatedContext, Task> OnAuthenticated { get; set; }
/// <summary>
/// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked.
/// </summary>
public Func<MicrosoftAccountReturnEndpointContext, Task> OnReturnEndpoint { get; set; }
/// <summary>
/// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked.
/// </summary>
public Action<MicrosoftAccountApplyRedirectContext> OnApplyRedirect { get; set; }
/// <summary>
/// Invoked whenever Microsoft succesfully authenticates a user
/// </summary>
@ -45,24 +34,5 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount
{
return OnAuthenticated(context);
}
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task ReturnEndpoint(MicrosoftAccountReturnEndpointContext context)
{
return OnReturnEndpoint(context);
}
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft account middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
public virtual void ApplyRedirect(MicrosoftAccountApplyRedirectContext context)
{
OnApplyRedirect(context);
}
}
}

View File

@ -5,6 +5,7 @@
"Microsoft.AspNet.RequestContainer": "1.0.0-*",
"Microsoft.AspNet.Security": "1.0.0-*",
"Microsoft.AspNet.Security.DataProtection": "1.0.0-*",
"Microsoft.AspNet.Security.OAuth": "1.0.0-*",
"Microsoft.AspNet.WebUtilities": "1.0.0-*",
"Microsoft.Framework.Logging": "1.0.0-*",
"Newtonsoft.Json": "5.0.8",
@ -13,7 +14,6 @@
"frameworks": {
"aspnet50": {
"dependencies": {
"System.Net.Http.WebRequest": ""
}
},
"aspnetcore50": {
@ -28,7 +28,6 @@
"System.IO": "4.0.10.0",
"System.IO.Compression": "4.0.0.0",
"System.Linq": "4.0.0.0",
"System.Net.Http.WinHttpHandler": "4.0.0.0",
"System.ObjectModel": "4.0.10.0",
"System.Reflection": "4.0.10.0",
"System.Resources.ResourceManager": "4.0.0.0",

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>4a636011-68ee-4ce5-836d-ea8e13cf71e4</ProjectGuid>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Console'">
<DebuggerFlavor>ConsoleDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Web'">
<DebuggerFlavor>WebDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNet.Security.OAuth
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
internal sealed class NotNullAttribute : Attribute
{
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// Specifies callback methods which the <see cref="OAuthAuthenticationMiddleware"/> invokes to enable developer control over the authentication process.
/// </summary>
public interface IOAuthAuthenticationNotifications
{
/// <summary>
/// Invoked after the provider successfully authenticates a user. This can be used to retrieve user information.
/// This notification may not be invoked by sub-classes of OAuthAuthenticationHandler if they override GetUserInformationAsync.
/// </summary>
/// <param name="context">Contains information about the login session.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task GetUserInformationAsync(OAuthGetUserInformationContext context);
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task ReturnEndpoint(OAuthReturnEndpointContext context);
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
void ApplyRedirect(OAuthApplyRedirectContext context);
}
}

View File

@ -5,22 +5,20 @@ using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Notifications;
namespace Microsoft.AspNet.Security.MicrosoftAccount
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// Context passed when a Challenge causes a redirect to authorize endpoint in the Microsoft account middleware.
/// </summary>
public class MicrosoftAccountApplyRedirectContext : BaseContext<MicrosoftAccountAuthenticationOptions>
public class OAuthApplyRedirectContext : BaseContext<OAuthAuthenticationOptions>
{
/// <summary>
/// Creates a new context object.
/// </summary>
/// <param name="context">The HTTP request context.</param>
/// <param name="options">The Microsoft account middleware options.</param>
/// <param name="properties">The authentication properties of the challenge.</param>
/// <param name="redirectUri">The initial redirect URI.</param>
public MicrosoftAccountApplyRedirectContext(HttpContext context, MicrosoftAccountAuthenticationOptions options,
AuthenticationProperties properties, string redirectUri)
public OAuthApplyRedirectContext(HttpContext context, OAuthAuthenticationOptions options, AuthenticationProperties properties, string redirectUri)
: base(context, options)
{
RedirectUri = redirectUri;

View File

@ -0,0 +1,77 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Net.Http;
using System.Security.Claims;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Notifications;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class OAuthAuthenticatedContext : BaseContext<OAuthAuthenticationOptions>
{
/// <summary>
/// Initializes a new <see cref="OAuthAuthenticatedContext"/>.
/// </summary>
/// <param name="context">The HTTP environment.</param>
/// <param name="user">The JSON-serialized user.</param>
/// <param name="tokens">The tokens returned from the token endpoint.</param>
public OAuthAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user,
TokenResponse tokens)
: base(context, options)
{
User = user;
AccessToken = tokens.AccessToken;
TokenType = tokens.TokenType;
RefreshToken = tokens.RefreshToken;
int expiresInValue;
if (Int32.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresInValue))
{
ExpiresIn = TimeSpan.FromSeconds(expiresInValue);
}
}
/// <summary>
/// Gets the JSON-serialized user.
/// </summary>
public JObject User { get; protected set; }
/// <summary>
/// Gets the access token provided by the authentication service.
/// </summary>
public string AccessToken { get; protected set; }
/// <summary>
/// Gets the access token type provided by the authentication service.
/// </summary>
public string TokenType { get; protected set; }
/// <summary>
/// Gets the refresh token provided by the authentication service.
/// </summary>
public string RefreshToken { get; protected set; }
/// <summary>
/// Gets the access token expiration time.
/// </summary>
public TimeSpan? ExpiresIn { get; protected set; }
/// <summary>
/// Gets the <see cref="ClaimsIdentity"/> representing the user.
/// </summary>
public ClaimsIdentity Identity { get; set; }
/// <summary>
/// Gets or sets a property bag for common authentication properties.
/// </summary>
public AuthenticationProperties Properties { get; set; }
}
}

View File

@ -0,0 +1,68 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// Default <see cref="IOAuthAuthenticationNotifications"/> implementation.
/// </summary>
public class OAuthAuthenticationNotifications : IOAuthAuthenticationNotifications
{
/// <summary>
/// Initializes a new <see cref="OAuthAuthenticationNotifications"/>
/// </summary>
public OAuthAuthenticationNotifications()
{
OnGetUserInformationAsync = OAuthAuthenticationDefaults.DefaultOnGetUserInformationAsync;
OnReturnEndpoint = context => Task.FromResult(0);
OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri);
}
/// <summary>
/// Gets or sets the function that is invoked when the Authenticated method is invoked.
/// </summary>
public Func<OAuthGetUserInformationContext, Task> OnGetUserInformationAsync { get; set; }
/// <summary>
/// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked.
/// </summary>
public Func<OAuthReturnEndpointContext, Task> OnReturnEndpoint { get; set; }
/// <summary>
/// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked.
/// </summary>
public Action<OAuthApplyRedirectContext> OnApplyRedirect { get; set; }
/// <summary>
/// Invoked after the provider successfully authenticates a user.
/// </summary>
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task GetUserInformationAsync(OAuthGetUserInformationContext context)
{
return OnGetUserInformationAsync(context);
}
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task ReturnEndpoint(OAuthReturnEndpointContext context)
{
return OnReturnEndpoint(context);
}
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the OAuth middleware.
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge.</param>
public virtual void ApplyRedirect(OAuthApplyRedirectContext context)
{
OnApplyRedirect(context);
}
}
}

View File

@ -0,0 +1,76 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Net.Http;
using System.Security.Claims;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Notifications;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class OAuthGetUserInformationContext : BaseContext<OAuthAuthenticationOptions>
{
/// <summary>
/// Initializes a new <see cref="OAuthGetUserInformationContext"/>.
/// </summary>
/// <param name="context">The HTTP environment.</param>
/// <param name="user">The JSON-serialized user.</param>
/// <param name="tokens">The tokens returned from the token endpoint.</param>
public OAuthGetUserInformationContext(HttpContext context, OAuthAuthenticationOptions options, HttpClient backchannel, TokenResponse tokens)
: base(context, options)
{
AccessToken = tokens.AccessToken;
TokenType = tokens.TokenType;
RefreshToken = tokens.RefreshToken;
Backchannel = backchannel;
int expiresInValue;
if (Int32.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresInValue))
{
ExpiresIn = TimeSpan.FromSeconds(expiresInValue);
}
}
/// <summary>
/// Gets the access token provided by the authentication service.
/// </summary>
public string AccessToken { get; protected set; }
/// <summary>
/// Gets the access token type provided by the authentication service.
/// </summary>
public string TokenType { get; protected set; }
/// <summary>
/// Gets the refresh token provided by the authentication service.
/// </summary>
public string RefreshToken { get; protected set; }
/// <summary>
/// Gets the access token expiration time.
/// </summary>
public TimeSpan? ExpiresIn { get; protected set; }
/// <summary>
/// Gets the backchannel used to communicate with the provider.
/// </summary>
public HttpClient Backchannel { get; protected set; }
/// <summary>
/// Gets the <see cref="ClaimsIdentity"/> representing the user.
/// </summary>
public ClaimsIdentity Identity { get; set; }
/// <summary>
/// Gets or sets a property bag for common authentication properties.
/// </summary>
public AuthenticationProperties Properties { get; set; }
}
}

View File

@ -4,19 +4,19 @@
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Security.Notifications;
namespace Microsoft.AspNet.Security.MicrosoftAccount
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// Provides context information to middleware providers.
/// </summary>
public class MicrosoftAccountReturnEndpointContext : ReturnEndpointContext
public class OAuthReturnEndpointContext : ReturnEndpointContext
{
/// <summary>
/// Initializes a new <see cref="MicrosoftAccountReturnEndpointContext"/>.
/// Initializes a new <see cref="OAuthReturnEndpointContext"/>.
/// </summary>
/// <param name="context">The HTTP environment.</param>
/// <param name="ticket">The authentication ticket.</param>
public MicrosoftAccountReturnEndpointContext(
public OAuthReturnEndpointContext(
HttpContext context,
AuthenticationTicket ticket)
: base(context, ticket)

View File

@ -0,0 +1,39 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Security.OAuth
{
public static class OAuthAuthenticationDefaults
{
public static readonly Func<OAuthGetUserInformationContext, Task> DefaultOnGetUserInformationAsync = context =>
{
// If the developer doesn't specify a user-info callback, just give them the tokens.
var identity = new ClaimsIdentity(
context.Options.AuthenticationType,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim("access_token", context.AccessToken, ClaimValueTypes.String, context.Options.AuthenticationType));
if (!string.IsNullOrEmpty(context.RefreshToken))
{
identity.AddClaim(new Claim("refresh_token", context.RefreshToken, ClaimValueTypes.String, context.Options.AuthenticationType));
}
if (!string.IsNullOrEmpty(context.TokenType))
{
identity.AddClaim(new Claim("token_type", context.TokenType, ClaimValueTypes.String, context.Options.AuthenticationType));
}
if (context.ExpiresIn.HasValue)
{
identity.AddClaim(new Claim("expires_in", context.ExpiresIn.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture),
ClaimValueTypes.String, context.Options.AuthenticationType));
}
context.Identity = identity;
return Task.FromResult(0);
};
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using Microsoft.AspNet.Security.OAuth;
namespace Microsoft.AspNet.Builder
{
/// <summary>
/// Extension methods for using <see cref="OAuthAuthenticationMiddleware"/>
/// </summary>
public static class OAuthAuthenticationExtensions
{
/// <summary>
/// Authenticate users using OAuth.
/// </summary>
/// <param name="app">The <see cref="IBuilder"/> passed to the configure method.</param>
/// <param name="options">The middleware configuration options.</param>
/// <returns>The updated <see cref="IBuilder"/>.</returns>
public static IBuilder UseOAuthAuthentication([NotNull] this IBuilder app, [NotNull] OAuthAuthenticationOptions<IOAuthAuthenticationNotifications> options)
{
if (string.IsNullOrEmpty(options.SignInAsAuthenticationType))
{
options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType();
}
if (options.Notifications == null)
{
options.Notifications = new OAuthAuthenticationNotifications();
}
return app.UseMiddleware<OAuthAuthenticationMiddleware<OAuthAuthenticationOptions<IOAuthAuthenticationNotifications>, IOAuthAuthenticationNotifications>>(options);
}
}
}

View File

@ -0,0 +1,253 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Infrastructure;
using Microsoft.AspNet.WebUtilities;
using Microsoft.Framework.Logging;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.OAuth
{
public class OAuthAuthenticationHandler<TOptions, TNotifications> : AuthenticationHandler<TOptions>
where TOptions : OAuthAuthenticationOptions<TNotifications>
where TNotifications : IOAuthAuthenticationNotifications
{
public OAuthAuthenticationHandler(HttpClient backchannel, ILogger logger)
{
Backchannel = backchannel;
Logger = logger;
}
protected HttpClient Backchannel { get; private set; }
protected ILogger Logger { get; private set; }
public override async Task<bool> InvokeAsync()
{
if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path)
{
return await InvokeReturnPathAsync();
}
return false;
}
public async Task<bool> InvokeReturnPathAsync()
{
AuthenticationTicket ticket = await AuthenticateAsync();
if (ticket == null)
{
Logger.WriteWarning("Invalid return state, unable to redirect.");
Response.StatusCode = 500;
return true;
}
var context = new OAuthReturnEndpointContext(Context, ticket)
{
SignInAsAuthenticationType = Options.SignInAsAuthenticationType,
RedirectUri = ticket.Properties.RedirectUri,
};
ticket.Properties.RedirectUri = null;
await Options.Notifications.ReturnEndpoint(context);
if (context.SignInAsAuthenticationType != null && context.Identity != null)
{
ClaimsIdentity signInIdentity = context.Identity;
if (!string.Equals(signInIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
{
signInIdentity = new ClaimsIdentity(signInIdentity.Claims, context.SignInAsAuthenticationType, signInIdentity.NameClaimType, signInIdentity.RoleClaimType);
}
Context.Response.SignIn(context.Properties, signInIdentity);
}
if (!context.IsRequestCompleted && context.RedirectUri != null)
{
if (context.Identity == null)
{
// add a redirect hint that sign-in failed in some way
context.RedirectUri = QueryHelpers.AddQueryString(context.RedirectUri, "error", "access_denied");
}
Response.Redirect(context.RedirectUri);
context.RequestCompleted();
}
return context.IsRequestCompleted;
}
protected override AuthenticationTicket AuthenticateCore()
{
return AuthenticateCoreAsync().Result;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
AuthenticationProperties properties = null;
try
{
IReadableStringCollection query = Request.Query;
// TODO: Is this a standard error returned by servers?
var value = query.Get("error");
if (!string.IsNullOrEmpty(value))
{
Logger.WriteVerbose("Remote server returned an error: " + Request.QueryString);
// TODO: Fail request rather than passing through?
return null;
}
string code = query.Get("code");
string state = query.Get("state");
properties = Options.StateDataFormat.Unprotect(state);
if (properties == null)
{
return null;
}
// OAuth2 10.12 CSRF
if (!ValidateCorrelationId(properties, Logger))
{
return new AuthenticationTicket(null, properties);
}
if (string.IsNullOrEmpty(code))
{
// Null if the remote server returns an error.
return new AuthenticationTicket(null, properties);
}
string requestPrefix = Request.Scheme + "://" + Request.Host;
string redirectUri = requestPrefix + RequestPathBase + Options.CallbackPath;
var tokens = await ExchangeCodeAsync(code, redirectUri);
if (string.IsNullOrWhiteSpace(tokens.AccessToken))
{
Logger.WriteWarning("Access token was not found");
return new AuthenticationTicket(null, properties);
}
return await GetUserInformationAsync(properties, tokens);
}
catch (Exception ex)
{
Logger.WriteError("Authentication failed", ex);
return new AuthenticationTicket(null, properties);
}
}
protected virtual async Task<TokenResponse> ExchangeCodeAsync(string code, string redirectUri)
{
var tokenRequestParameters = new Dictionary<string, string>()
{
{ "client_id", Options.ClientId },
{ "redirect_uri", redirectUri },
{ "client_secret", Options.ClientSecret },
{ "code", code },
{ "grant_type", "authorization_code" },
};
var requestContent = new FormUrlEncodedContent(tokenRequestParameters);
var requestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
requestMessage.Content = requestContent;
HttpResponseMessage response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
response.EnsureSuccessStatusCode();
string oauthTokenResponse = await response.Content.ReadAsStringAsync();
JObject oauth2Token = JObject.Parse(oauthTokenResponse);
return new TokenResponse(oauth2Token);
}
protected virtual async Task<AuthenticationTicket> GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens)
{
var context = new OAuthGetUserInformationContext(Context, Options, Backchannel, tokens)
{
Properties = properties,
};
await Options.Notifications.GetUserInformationAsync(context);
return new AuthenticationTicket(context.Identity, context.Properties);
}
protected override void ApplyResponseChallenge()
{
if (Response.StatusCode != 401)
{
return;
}
// Active middleware should redirect on 401 even if there wasn't an explicit challenge.
if (ChallengeContext == null && Options.AuthenticationMode == AuthenticationMode.Passive)
{
return;
}
string baseUri = Request.Scheme + "://" + Request.Host + Request.PathBase;
string currentUri = baseUri + Request.Path + Request.QueryString;
string redirectUri = baseUri + Options.CallbackPath;
AuthenticationProperties properties;
if (ChallengeContext == null)
{
properties = new AuthenticationProperties();
}
else
{
properties = new AuthenticationProperties(ChallengeContext.Properties);
}
if (string.IsNullOrEmpty(properties.RedirectUri))
{
properties.RedirectUri = currentUri;
}
// OAuth2 10.12 CSRF
GenerateCorrelationId(properties);
string authorizationEndpoint = BuildChallengeUrl(properties, redirectUri);
var redirectContext = new OAuthApplyRedirectContext(
Context, Options,
properties, authorizationEndpoint);
Options.Notifications.ApplyRedirect(redirectContext);
}
protected virtual string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
string scope = FormatScope();
string state = Options.StateDataFormat.Protect(properties);
var queryBuilder = new QueryBuilder()
{
{ "client_id", Options.ClientId },
{ "scope", scope },
{ "response_type", "code" },
{ "redirect_uri", redirectUri },
{ "state", state },
};
return Options.AuthorizationEndpoint + queryBuilder.ToString();
}
protected virtual string FormatScope()
{
// OAuth2 3.3 space separated
return string.Join(" ", Options.Scope);
}
protected override void ApplyResponseGrant()
{
// N/A - No SignIn or SignOut support.
}
}
}

View File

@ -0,0 +1,105 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Http;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Security.DataHandler;
using Microsoft.AspNet.Security.DataProtection;
using Microsoft.AspNet.Security.Infrastructure;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// An ASP.NET middleware for authenticating users using OAuth services.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")]
public class OAuthAuthenticationMiddleware<TOptions, TNotifications> : AuthenticationMiddleware<TOptions>
where TOptions : OAuthAuthenticationOptions<TNotifications>
where TNotifications : IOAuthAuthenticationNotifications
{
/// <summary>
/// Initializes a new <see cref="OAuthAuthenticationMiddleware"/>.
/// </summary>
/// <param name="next">The next middleware in the HTTP pipeline to invoke.</param>
/// <param name="dataProtectionProvider"></param>
/// <param name="loggerFactory"></param>
/// <param name="options">Configuration options for the middleware.</param>
public OAuthAuthenticationMiddleware(
RequestDelegate next,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
TOptions options)
: base(next, options)
{
if (string.IsNullOrWhiteSpace(Options.ClientId))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientId"));
}
if (string.IsNullOrWhiteSpace(Options.ClientSecret))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientSecret"));
}
if (string.IsNullOrWhiteSpace(Options.AuthorizationEndpoint))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "AuthorizationEndpoint"));
}
if (string.IsNullOrWhiteSpace(Options.TokenEndpoint))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "TokenEndpoint"));
}
Logger = loggerFactory.Create(this.GetType().FullName);
if (Options.StateDataFormat == null)
{
IDataProtector dataProtector = DataProtectionHelpers.CreateDataProtector(dataProtectionProvider,
this.GetType().FullName, options.AuthenticationType, "v1");
Options.StateDataFormat = new PropertiesDataFormat(dataProtector);
}
Backchannel = new HttpClient(ResolveHttpMessageHandler(Options));
Backchannel.Timeout = Options.BackchannelTimeout;
Backchannel.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB
}
protected HttpClient Backchannel { get; private set; }
protected ILogger Logger { get; private set; }
/// <summary>
/// Provides the <see cref="AuthenticationHandler"/> object for processing authentication-related requests.
/// </summary>
/// <returns>An <see cref="AuthenticationHandler"/> configured with the <see cref="OAuthAuthenticationOptions"/> supplied to the constructor.</returns>
protected override AuthenticationHandler<TOptions> CreateHandler()
{
return new OAuthAuthenticationHandler<TOptions, TNotifications>(Backchannel, Logger);
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")]
private static HttpMessageHandler ResolveHttpMessageHandler(OAuthAuthenticationOptions<TNotifications> options)
{
HttpMessageHandler handler = options.BackchannelHttpHandler ??
#if ASPNET50
new WebRequestHandler();
// If they provided a validator, apply it or fail.
if (options.BackchannelCertificateValidator != null)
{
// Set the cert validate callback
var webRequestHandler = handler as WebRequestHandler;
if (webRequestHandler == null)
{
throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch);
}
webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate;
}
#else
new WinHttpHandler();
#endif
return handler;
}
}
}

View File

@ -0,0 +1,117 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// Configuration options for <see cref="OAuthAuthenticationMiddleware"/>.
/// </summary>
public class OAuthAuthenticationOptions : AuthenticationOptions
{
/// <summary>
/// Initializes a new <see cref="OAuthAuthenticationOptions"/>.
/// </summary>
public OAuthAuthenticationOptions([NotNull] string authenticationType)
: base(authenticationType)
{
Caption = authenticationType;
AuthenticationMode = AuthenticationMode.Passive;
Scope = new List<string>();
BackchannelTimeout = TimeSpan.FromSeconds(60);
}
/// <summary>
/// Gets or sets the provider-assigned client id.
/// </summary>
public string ClientId { get; set; }
/// <summary>
/// Gets or sets the provider-assigned client secret.
/// </summary>
public string ClientSecret { get; set; }
/// <summary>
/// Gets or sets the URI where the client will be redirected to authenticate.
/// </summary>
public string AuthorizationEndpoint { get; set; }
/// <summary>
/// Gets or sets the URI the middleware will access to exchange the OAuth token.
/// </summary>
public string TokenEndpoint { get; set; }
/// <summary>
/// Gets or sets the URI the middleware will access to obtain the user information.
/// This value is not used in the default implementation, it is for use in custom implementations of
/// IOAuthAuthenticationNotifications.GetUserInformationAsync or OAuthAuthenticationHandler.GetUserInformationAsync.
/// </summary>
public string UserInformationEndpoint { get; set; }
#if ASPNET50
/// <summary>
/// Gets or sets the a pinned certificate validator to use to validate the endpoints used
/// in back channel communications belong to the auth provider.
/// </summary>
/// <value>
/// The pinned certificate validator.
/// </value>
/// <remarks>If this property is null then the default certificate checks are performed,
/// validating the subject name and if the signing chain is a trusted party.</remarks>
public ICertificateValidator BackchannelCertificateValidator { get; set; }
#endif
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
/// <remarks>
/// The default value is the authentication type.
/// </remarks>
public string Caption
{
get { return Description.Caption; }
set { Description.Caption = value; }
}
/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with the auth provider.
/// </summary>
/// <value>
/// The back channel timeout.
/// </value>
public TimeSpan BackchannelTimeout { get; set; }
/// <summary>
/// The HttpMessageHandler used to communicate with the auth provider.
/// This cannot be set at the same time as BackchannelCertificateValidator unless the value
/// can be downcast to a WebRequestHandler.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// A list of permissions to request.
/// </summary>
public IList<string> Scope { get; private set; }
/// <summary>
/// The request path within the application's base path where the user-agent will be returned.
/// The middleware will process this request when it arrives.
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public string SignInAsAuthenticationType { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>
public ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; set; }
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Security.OAuth
{
/// <summary>
/// Configuration options for <see cref="OAuthAuthenticationMiddleware"/>.
/// </summary>
public class OAuthAuthenticationOptions<TNotifications> : OAuthAuthenticationOptions where TNotifications : IOAuthAuthenticationNotifications
{
/// <summary>
/// Initializes a new <see cref="OAuthAuthenticationOptions"/>.
/// </summary>
public OAuthAuthenticationOptions([NotNull] string authenticationType)
: base(authenticationType)
{
}
/// <summary>
/// Gets or sets the <see cref="IOAuthAuthenticationNotifications"/> used to handle authentication events.
/// </summary>
public TNotifications Notifications { get; set; }
}
}

View File

@ -0,0 +1,83 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.33440
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.AspNet.Security.OAuth {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Security.OAuth.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to The &apos;{0}&apos; option must be provided..
/// </summary>
internal static string Exception_OptionMustBeProvided
{
get
{
return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler..
/// </summary>
internal static string Exception_ValidatorHandlerMismatch {
get {
return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Exception_OptionMustBeProvided" xml:space="preserve">
<value>The '{0}' option must be provided.</value>
</data>
<data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
<value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
</data>
</root>

View File

@ -0,0 +1,25 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNet.Security.OAuth
{
public class TokenResponse
{
public TokenResponse(JObject response)
{
Response = response;
AccessToken = response.Value<string>("access_token");
TokenType = response.Value<string>("token_type");
RefreshToken = response.Value<string>("refresh_token");
ExpiresIn = response.Value<string>("expires_in");
}
public JObject Response { get; set; }
public string AccessToken { get; set; }
public string TokenType { get; set; }
public string RefreshToken { get; set; }
public string ExpiresIn { get; set; }
}
}

View File

@ -0,0 +1,46 @@
{
"version": "1.0.0-*",
"dependencies": {
"Microsoft.AspNet.Http": "1.0.0-*",
"Microsoft.AspNet.RequestContainer": "1.0.0-*",
"Microsoft.AspNet.Security": "1.0.0-*",
"Microsoft.AspNet.Security.DataProtection": "1.0.0-*",
"Microsoft.AspNet.WebUtilities": "1.0.0-*",
"Microsoft.Framework.Logging": "1.0.0-*",
"Newtonsoft.Json": "5.0.8",
"System.Net.Http": "4.0.0.0"
},
"frameworks": {
"aspnet50": {
"dependencies": {
"System.Net.Http.WebRequest": ""
}
},
"aspnetcore50": {
"dependencies": {
"System.Collections": "4.0.10.0",
"System.ComponentModel": "4.0.0.0",
"System.Console": "4.0.0.0",
"System.Diagnostics.Debug": "4.0.10.0",
"System.Diagnostics.Tools": "4.0.0.0",
"System.Dynamic.Runtime": "4.0.0.0",
"System.Globalization": "4.0.10.0",
"System.IO": "4.0.10.0",
"System.IO.Compression": "4.0.0.0",
"System.Linq": "4.0.0.0",
"System.Net.Http.WinHttpHandler": "4.0.0.0",
"System.ObjectModel": "4.0.0.0",
"System.Reflection": "4.0.10.0",
"System.Resources.ResourceManager": "4.0.0.0",
"System.Runtime": "4.0.20.0",
"System.Runtime.Extensions": "4.0.10.0",
"System.Runtime.InteropServices": "4.0.20.0",
"System.Security.Claims": "1.0.0-*",
"System.Security.Cryptography.Hashing.Algorithms": "4.0.0.0",
"System.Security.Principal": "4.0.0.0",
"System.Threading": "4.0.0.0",
"System.Threading.Tasks": "4.0.10.0"
}
}
}
}

View File

@ -328,9 +328,9 @@ namespace Microsoft.AspNet.Security.Twitter
var request = new HttpRequestMessage(HttpMethod.Post, AccessTokenEndpoint);
request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString());
var formPairs = new List<KeyValuePair<string, string>>()
var formPairs = new Dictionary<string, string>()
{
new KeyValuePair<string, string>("oauth_verifier", verifier)
{ "oauth_verifier", verifier },
};
request.Content = new FormUrlEncodedContent(formPairs);

View File

@ -62,11 +62,11 @@ namespace Microsoft.AspNet.Security.Facebook
transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
var location = transaction.Response.Headers.Location.AbsoluteUri;
location.ShouldContain("https://www.facebook.com/dialog/oauth");
location.ShouldContain("?response_type=code");
location.ShouldContain("&client_id=");
location.ShouldContain("&redirect_uri=");
location.ShouldContain("&scope=");
location.ShouldContain("&state=");
location.ShouldContain("response_type=code");
location.ShouldContain("client_id=");
location.ShouldContain("redirect_uri=");
location.ShouldContain("scope=");
location.ShouldContain("state=");
}
private static TestServer CreateServer(Action<IBuilder> configure, Func<HttpContext, bool> handler)