#49 - OAuth base middleware.
This commit is contained in:
parent
214b82ea01
commit
80c8891c08
13
Security.sln
13
Security.sln
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 '{0}' 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue