From 80c8891c0854edaecf658f6972c524ef21cd15fa Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Wed, 3 Sep 2014 11:48:38 -0700 Subject: [PATCH] #49 - OAuth base middleware. --- Security.sln | 13 + samples/SocialSample/Startup.cs | 93 ++++++ .../FacebookAuthenticationDefaults.cs | 6 + .../FacebookAuthenticationHandler.cs | 302 ++++------------- .../FacebookAuthenticationMiddleware.cs | 49 +-- .../FacebookAuthenticationOptions.cs | 89 +---- .../FacebookApplyRedirectContext.cs | 43 --- .../FacebookAuthenticatedContext.cs | 50 +-- .../FacebookAuthenticationNotifications.cs | 35 +- .../FacebookReturnEndpointContext.cs | 26 -- .../IFacebookAuthenticationNotifications.cs | 16 +- .../Resources.Designer.cs | 9 - .../Resources.resx | 3 - .../project.json | 3 +- .../GoogleAuthenticationDefaults.cs | 6 + .../GoogleAuthenticationHandler.cs | 306 ++++-------------- .../GoogleAuthenticationMiddleware.cs | 62 +--- .../GoogleAuthenticationOptions.cs | 85 +---- .../GoogleApplyRedirectContext.cs | 43 --- .../GoogleAuthenticatedContext.cs | 60 +--- .../GoogleAuthenticationNotifications.cs | 34 +- .../GoogleReturnEndpointContext.cs | 26 -- .../IGoogleAuthenticationNotifications.cs | 16 +- .../project.json | 3 +- .../MicrosoftAccountAuthenticationDefaults.cs | 6 + .../MicrosoftAccountAuthenticationHandler.cs | 251 ++------------ ...icrosoftAccountAuthenticationMiddleware.cs | 60 +--- .../MicrosoftAccountAuthenticationOptions.cs | 90 +----- ...osoftAccountAuthenticationNotifications.cs | 16 +- .../MicrosoftAccountAuthenticatedContext.cs | 61 +--- ...osoftAccountAuthenticationNotifications.cs | 34 +- .../project.json | 3 +- .../Microsoft.AspNet.Security.OAuth.kproj | 28 ++ .../NotNullAttribute.cs | 12 + .../IOAuthAuthenticationNotifications.cs | 34 ++ .../OAuthApplyRedirectContext.cs} | 8 +- .../OAuthAuthenticatedContext.cs | 77 +++++ .../OAuthAuthenticationNotifications.cs | 68 ++++ .../OAuthGetUserInformationContext.cs | 76 +++++ .../OAuthReturnEndpointContext.cs} | 8 +- .../OAuthAuthenticationDefaults.cs | 39 +++ .../OAuthAuthenticationExtensions.cs | 34 ++ .../OAuthAuthenticationHandler.cs | 253 +++++++++++++++ .../OAuthAuthenticationMiddleware.cs | 105 ++++++ .../OAuthAuthenticationOptions.cs | 117 +++++++ .../OAuthAuthenticationOptions`1.cs | 24 ++ .../Resources.Designer.cs | 83 +++++ .../Resources.resx | 126 ++++++++ .../TokenResponse.cs | 25 ++ .../project.json | 46 +++ .../TwitterAuthenticationHandler.cs | 4 +- .../Facebook/FacebookMiddlewareTests.cs | 10 +- 52 files changed, 1519 insertions(+), 1557 deletions(-) delete mode 100644 src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs delete mode 100644 src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs delete mode 100644 src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs delete mode 100644 src/Microsoft.AspNet.Security.Google/Notifications/GoogleReturnEndpointContext.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/Microsoft.AspNet.Security.OAuth.kproj create mode 100644 src/Microsoft.AspNet.Security.OAuth/NotNullAttribute.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthAuthenticationNotifications.cs rename src/{Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountApplyRedirectContext.cs => Microsoft.AspNet.Security.OAuth/Notifications/OAuthApplyRedirectContext.cs} (74%) create mode 100644 src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthAuthenticatedContext.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthAuthenticationNotifications.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthGetUserInformationContext.cs rename src/{Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountReturnEndpointContext.cs => Microsoft.AspNet.Security.OAuth/Notifications/OAuthReturnEndpointContext.cs} (71%) create mode 100644 src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationDefaults.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationExtensions.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationHandler.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationOptions.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationOptions`1.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/Resources.resx create mode 100644 src/Microsoft.AspNet.Security.OAuth/TokenResponse.cs create mode 100644 src/Microsoft.AspNet.Security.OAuth/project.json diff --git a/Security.sln b/Security.sln index be6636fe28..c9951b0ddf 100644 --- a/Security.sln +++ b/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 diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index 03435fe0f6..f3aff738d8 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -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("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("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("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("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 => { diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationDefaults.cs index a9d35151c8..19cc380749 100644 --- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationDefaults.cs +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationDefaults.cs @@ -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"; } } diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs index 4dde8b75dc..6520902559 100644 --- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs @@ -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 + internal class FacebookAuthenticationHandler : OAuthAuthenticationHandler { - 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 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 AuthenticateCoreAsync() + protected override async Task 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 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 InvokeAsync() - { - return await InvokeReplyPathAsync(); - } - - private async Task 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); + } } } diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs index e67abf1c91..2d653d7c28 100644 --- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs @@ -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 /// /// An ASP.NET middleware for authenticating users using Facebook. /// - [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware is not disposable.")] - public class FacebookAuthenticationMiddleware : AuthenticationMiddleware + public class FacebookAuthenticationMiddleware : OAuthAuthenticationMiddleware { - private readonly ILogger _logger; - private readonly HttpClient _httpClient; - /// /// Initializes a new . /// @@ -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 } /// @@ -69,30 +51,7 @@ namespace Microsoft.AspNet.Security.Facebook /// An configured with the supplied to the constructor. protected override AuthenticationHandler 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); } } } diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs index 1d4fa9a54c..7f1199a086 100644 --- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs @@ -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 { /// /// Configuration options for . /// - public class FacebookAuthenticationOptions : AuthenticationOptions + public class FacebookAuthenticationOptions : OAuthAuthenticationOptions { /// /// Initializes a new . /// - [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(); - BackchannelTimeout = TimeSpan.FromSeconds(60); SendAppSecretProof = true; + AuthorizationEndpoint = FacebookAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = FacebookAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = FacebookAuthenticationDefaults.UserInformationEndpoint; } + // Facebook uses a non-standard term for this field. /// /// Gets or sets the Facebook-assigned appId. /// - public string AppId { get; set; } + public string AppId + { + get { return ClientId; } + set { ClientId = value; } + } + // Facebook uses a non-standard term for this field. /// /// Gets or sets the Facebook-assigned app secret. /// - public string AppSecret { get; set; } -#if ASPNET50 - /// - /// Gets or sets the a pinned certificate validator to use to validate the endpoints used - /// in back channel communications belong to Facebook. - /// - /// - /// The pinned certificate validator. - /// - /// 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. - public ICertificateValidator BackchannelCertificateValidator { get; set; } -#endif - /// - /// Gets or sets timeout value in milliseconds for back channel communications with Facebook. - /// - /// - /// The back channel timeout in milliseconds. - /// - public TimeSpan BackchannelTimeout { get; set; } - - /// - /// 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. - /// - public HttpMessageHandler BackchannelHttpHandler { get; set; } - - /// - /// Get or sets the text that the user can display on a sign in user interface. - /// - public string Caption + public string AppSecret { - get { return Description.Caption; } - set { Description.Caption = value; } + get { return ClientSecret; } + set { ClientSecret = value; } } - /// - /// 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". - /// - public PathString CallbackPath { get; set; } - - /// - /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user . - /// - public string SignInAsAuthenticationType { get; set; } - - /// - /// Gets or sets the used to handle authentication events. - /// - public IFacebookAuthenticationNotifications Notifications { get; set; } - - /// - /// Gets or sets the type used to secure data handled by the middleware. - /// - public ISecureDataFormat StateDataFormat { get; set; } - - /// - /// A list of permissions to request. - /// - public IList Scope { get; private set; } - /// /// Gets or sets if the appsecret_proof should be generated and sent with Facebook API calls. /// This is enabled by default. diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs deleted file mode 100644 index e3e1b35b3e..0000000000 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs +++ /dev/null @@ -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 -{ - /// - /// The Context passed when a Challenge causes a redirect to authorize endpoint in the Facebook middleware. - /// - public class FacebookApplyRedirectContext : BaseContext - { - /// - /// Creates a new context object. - /// - /// The http request context. - /// The Facebook middleware options. - /// The authentication properties of the challenge. - /// The initial redirect URI. - [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; - } - - /// - /// Gets the URI used for the redirect operation. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Represents header value")] - public string RedirectUri { get; private set; } - - /// - /// Gets the authentication properties of the challenge. - /// - public AuthenticationProperties Properties { get; private set; } - } -} diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs index 6797ed4959..aa4e07daaf 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs @@ -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 /// /// Contains information about the login session as well as the user . /// - public class FacebookAuthenticatedContext : BaseContext + public class FacebookAuthenticatedContext : OAuthAuthenticatedContext { /// /// Initializes a new . /// /// The HTTP environment. /// The JSON-serialized user. - /// The Facebook Access token. - /// Seconds until expiration. - public FacebookAuthenticatedContext(HttpContext context, JObject user, string accessToken, string expires) - : base(context) + /// The Facebook Access token. + 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"); } - /// - /// Gets the JSON-serialized user. - /// - public JObject User { get; private set; } - - /// - /// Gets the Facebook access token. - /// - public string AccessToken { get; private set; } - - /// - /// Gets the Facebook access token expiration time. - /// - public TimeSpan? ExpiresIn { get; set; } - /// /// Gets the Facebook user ID. /// @@ -82,16 +54,6 @@ namespace Microsoft.AspNet.Security.Facebook /// public string Email { get; private set; } - /// - /// Gets the representing the user. - /// - public ClaimsIdentity Identity { get; set; } - - /// - /// Gets or sets a property bag for common authentication properties. - /// - public AuthenticationProperties Properties { get; set; } - private static string TryGetValue(JObject user, string propertyName) { JToken value; diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs index b9b33b1257..f86287cc11 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs @@ -3,13 +3,14 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNet.Security.OAuth; namespace Microsoft.AspNet.Security.Facebook { /// /// The default implementation. /// - public class FacebookAuthenticationNotifications : IFacebookAuthenticationNotifications + public class FacebookAuthenticationNotifications : OAuthAuthenticationNotifications, IFacebookAuthenticationNotifications { /// /// Initializes a new . @@ -17,9 +18,6 @@ namespace Microsoft.AspNet.Security.Facebook public FacebookAuthenticationNotifications() { OnAuthenticated = context => Task.FromResult(null); - OnReturnEndpoint = context => Task.FromResult(null); - OnApplyRedirect = context => - context.Response.Redirect(context.RedirectUri); } /// @@ -27,16 +25,6 @@ namespace Microsoft.AspNet.Security.Facebook /// public Func OnAuthenticated { get; set; } - /// - /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. - /// - public Func OnReturnEndpoint { get; set; } - - /// - /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. - /// - public Action OnApplyRedirect { get; set; } - /// /// Invoked whenever Facebook succesfully authenticates a user. /// @@ -46,24 +34,5 @@ namespace Microsoft.AspNet.Security.Facebook { return OnAuthenticated(context); } - - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// - /// A representing the completed operation. - public virtual Task ReturnEndpoint(FacebookReturnEndpointContext context) - { - return OnReturnEndpoint(context); - } - - /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware. - /// - /// Contains redirect URI and of the challenge. - public virtual void ApplyRedirect(FacebookApplyRedirectContext context) - { - OnApplyRedirect(context); - } } } diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs deleted file mode 100644 index 29786a9068..0000000000 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs +++ /dev/null @@ -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 -{ - /// - /// Provides context information for notifications. - /// - public class FacebookReturnEndpointContext : ReturnEndpointContext - { - /// - /// Creates a new context object. - /// - /// The http environment. - /// The authentication ticket. - public FacebookReturnEndpointContext( - HttpContext context, - AuthenticationTicket ticket) - : base(context, ticket) - { - } - } -} diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs index 236cba3ac5..0849a84a06 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs @@ -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 { /// /// Specifies callback methods which the invokes to enable developer control over the authentication process. /// - public interface IFacebookAuthenticationNotifications + public interface IFacebookAuthenticationNotifications : IOAuthAuthenticationNotifications { /// /// Invoked when Facebook succesfully authenticates a user. @@ -16,18 +17,5 @@ namespace Microsoft.AspNet.Security.Facebook /// Contains information about the login session as well as the user . /// A representing the completed operation. Task Authenticated(FacebookAuthenticatedContext context); - - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// - /// A representing the completed operation. - Task ReturnEndpoint(FacebookReturnEndpointContext context); - - /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware. - /// - /// Contains redirect URI and of the challenge. - void ApplyRedirect(FacebookApplyRedirectContext context); } } diff --git a/src/Microsoft.AspNet.Security.Facebook/Resources.Designer.cs b/src/Microsoft.AspNet.Security.Facebook/Resources.Designer.cs index dca281f828..18a738d708 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Resources.Designer.cs @@ -68,14 +68,5 @@ namespace Microsoft.AspNet.Security.Facebook { return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); } } - - /// - /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. - /// - internal static string Exception_ValidatorHandlerMismatch { - get { - return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); - } - } } } diff --git a/src/Microsoft.AspNet.Security.Facebook/Resources.resx b/src/Microsoft.AspNet.Security.Facebook/Resources.resx index 2a19bea96a..56ef7f56bd 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Resources.resx +++ b/src/Microsoft.AspNet.Security.Facebook/Resources.resx @@ -120,7 +120,4 @@ The '{0}' option must be provided. - - An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. - \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Facebook/project.json b/src/Microsoft.AspNet.Security.Facebook/project.json index bf9da22531..fbbb32c78a 100644 --- a/src/Microsoft.AspNet.Security.Facebook/project.json +++ b/src/Microsoft.AspNet.Security.Facebook/project.json @@ -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", diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationDefaults.cs index b135063840..acf982906b 100644 --- a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationDefaults.cs +++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationDefaults.cs @@ -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"; } } diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationHandler.cs index fe2a4feab9..1d75c889f3 100644 --- a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationHandler.cs @@ -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 + internal class GoogleAuthenticationHandler : OAuthAuthenticationHandler { - 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 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 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 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>(); - body.Add(new KeyValuePair("grant_type", "authorization_code")); - body.Add(new KeyValuePair("code", code)); - body.Add(new KeyValuePair("redirect_uri", redirectUri)); - body.Add(new KeyValuePair("client_id", Options.ClientId)); - body.Add(new KeyValuePair("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("access_token"); - string expires = response.Value("expires_in"); - string refreshToken = response.Value("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(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 InvokeAsync() - { - return await InvokeReplyPathAsync(); - } - - private async Task 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 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. - } } } diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs index 66768cbb84..08d7610fa0 100644 --- a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs @@ -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. /// [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] - public class GoogleAuthenticationMiddleware : AuthenticationMiddleware + public class GoogleAuthenticationMiddleware : OAuthAuthenticationMiddleware { - private readonly ILogger _logger; - private readonly HttpClient _httpClient; - /// /// Initializes a new . /// @@ -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"); + } } /// @@ -69,30 +56,7 @@ namespace Microsoft.AspNet.Security.Google /// An configured with the supplied to the constructor. protected override AuthenticationHandler 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); } } } diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs index 1ede9f0425..0b8e3ab091 100644 --- a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs @@ -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 { /// /// Configuration options for . /// - public class GoogleAuthenticationOptions : AuthenticationOptions + public class GoogleAuthenticationOptions : OAuthAuthenticationOptions { /// /// Initializes a new . /// - [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(); - BackchannelTimeout = TimeSpan.FromSeconds(60); + AuthorizationEndpoint = GoogleAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = GoogleAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = GoogleAuthenticationDefaults.UserInformationEndpoint; } - /// - /// Gets or sets the Google-assigned client id. - /// - public string ClientId { get; set; } - - /// - /// Gets or sets the Google-assigned client secret. - /// - public string ClientSecret { get; set; } -#if ASPNET50 - /// - /// Gets or sets the a pinned certificate validator to use to validate the endpoints used - /// in back channel communications belong to Google. - /// - /// - /// The pinned certificate validator. - /// - /// 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. - public ICertificateValidator BackchannelCertificateValidator { get; set; } -#endif - /// - /// Gets or sets timeout value in milliseconds for back channel communications with Google. - /// - /// - /// The back channel timeout in milliseconds. - /// - public TimeSpan BackchannelTimeout { get; set; } - - /// - /// 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. - /// - public HttpMessageHandler BackchannelHttpHandler { get; set; } - - /// - /// Get or sets the text that the user can display on a sign in user interface. - /// - public string Caption - { - get { return Description.Caption; } - set { Description.Caption = value; } - } - - /// - /// 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". - /// - public PathString CallbackPath { get; set; } - - /// - /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user . - /// - public string SignInAsAuthenticationType { get; set; } - - /// - /// Gets or sets the used to handle authentication events. - /// - public IGoogleAuthenticationNotifications Notifications { get; set; } - - /// - /// Gets or sets the type used to secure data handled by the middleware. - /// - public ISecureDataFormat StateDataFormat { get; set; } - - /// - /// A list of permissions to request. - /// - public IList Scope { get; private set; } - /// /// access_type. Set to 'offline' to request a refresh token. /// diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs deleted file mode 100644 index 7df6358ddd..0000000000 --- a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs +++ /dev/null @@ -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 -{ - /// - /// The Context passed when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware. - /// - public class GoogleApplyRedirectContext : BaseContext - { - /// - /// Creates a new context object. - /// - /// The HTTP request context. - /// The Google OAuth 2.0 middleware options. - /// The authentication properties of the challenge. - /// The initial redirect URI. - [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; - } - - /// - /// Gets the URI used for the redirect operation. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Represents header value")] - public string RedirectUri { get; private set; } - - /// - /// Gets the authentication properties of the challenge. - /// - public AuthenticationProperties Properties { get; private set; } - } -} diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs index 84f535fb8d..10fa2b3706 100644 --- a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs +++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs @@ -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 /// /// Contains information about the login session as well as the user . /// - public class GoogleAuthenticatedContext : BaseContext + public class GoogleAuthenticatedContext : OAuthAuthenticatedContext { /// /// Initializes a new . /// /// The HTTP environment. /// The JSON-serialized Google user info. - /// Google OAuth 2.0 access token. - /// Goolge OAuth 2.0 refresh token. - /// Seconds until expiration. - public GoogleAuthenticatedContext(HttpContext context, JObject user, string accessToken, - string refreshToken, string expires) - : base(context) + /// Google OAuth 2.0 access token, refresh token, etc. + 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"); } - /// - /// Gets the JSON-serialized user. - /// - /// - /// Contains the Google user obtained from the userinfo endpoint. - /// - public JObject User { get; private set; } - - /// - /// Gets the Google access token. - /// - public string AccessToken { get; private set; } - - /// - /// Gets the Google refresh token. - /// - /// - /// This value is not null only when access_type authorize parameter is offline. - /// - public string RefreshToken { get; private set; } - - /// - /// Gets the Google access token expiration time. - /// - public TimeSpan? ExpiresIn { get; set; } - /// /// Gets the Google user ID. /// @@ -102,16 +64,6 @@ namespace Microsoft.AspNet.Security.Google /// public string Email { get; private set; } - /// - /// Gets the representing the user. - /// - public ClaimsIdentity Identity { get; set; } - - /// - /// Gets or sets a property bag for common authentication properties. - /// - public AuthenticationProperties Properties { get; set; } - private static string TryGetValue(JObject user, string propertyName) { JToken value; diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs index 36f9368953..ef2e5d3cec 100644 --- a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs @@ -3,13 +3,14 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNet.Security.OAuth; namespace Microsoft.AspNet.Security.Google { /// /// The default implementation. /// - public class GoogleAuthenticationNotifications : IGoogleAuthenticationNotifications + public class GoogleAuthenticationNotifications : OAuthAuthenticationNotifications, IGoogleAuthenticationNotifications { /// /// Initializes a new . @@ -17,8 +18,6 @@ namespace Microsoft.AspNet.Security.Google public GoogleAuthenticationNotifications() { OnAuthenticated = context => Task.FromResult(null); - OnReturnEndpoint = context => Task.FromResult(null); - OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri); } /// @@ -26,16 +25,6 @@ namespace Microsoft.AspNet.Security.Google /// public Func OnAuthenticated { get; set; } - /// - /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. - /// - public Func OnReturnEndpoint { get; set; } - - /// - /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. - /// - public Action OnApplyRedirect { get; set; } - /// /// Invoked whenever Google succesfully authenticates a user. /// @@ -45,24 +34,5 @@ namespace Microsoft.AspNet.Security.Google { return OnAuthenticated(context); } - - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// Contains context information and authentication ticket of the return endpoint. - /// A representing the completed operation. - public virtual Task ReturnEndpoint(GoogleReturnEndpointContext context) - { - return OnReturnEndpoint(context); - } - - /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware. - /// - /// Contains redirect URI and of the challenge. - public virtual void ApplyRedirect(GoogleApplyRedirectContext context) - { - OnApplyRedirect(context); - } } } diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleReturnEndpointContext.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleReturnEndpointContext.cs deleted file mode 100644 index ce6a13742e..0000000000 --- a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleReturnEndpointContext.cs +++ /dev/null @@ -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 -{ - /// - /// Provides context information to middleware notifications. - /// - public class GoogleReturnEndpointContext : ReturnEndpointContext - { - /// - /// Initialize a . - /// - /// The HTTP environment. - /// The authentication ticket. - public GoogleReturnEndpointContext( - HttpContext context, - AuthenticationTicket ticket) - : base(context, ticket) - { - } - } -} diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/IGoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Google/Notifications/IGoogleAuthenticationNotifications.cs index 13930346c8..9cf9d2dd53 100644 --- a/src/Microsoft.AspNet.Security.Google/Notifications/IGoogleAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Google/Notifications/IGoogleAuthenticationNotifications.cs @@ -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 { /// /// Specifies callback methods which the invokes to enable developer control over the authentication process. /// - public interface IGoogleAuthenticationNotifications + public interface IGoogleAuthenticationNotifications : IOAuthAuthenticationNotifications { /// /// Invoked whenever Google succesfully authenticates a user. @@ -16,18 +17,5 @@ namespace Microsoft.AspNet.Security.Google /// Contains information about the login session as well as the user . /// A representing the completed operation. Task Authenticated(GoogleAuthenticatedContext context); - - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// Contains context information and authentication ticket of the return endpoint. - /// A representing the completed operation. - Task ReturnEndpoint(GoogleReturnEndpointContext context); - - /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware. - /// - /// Contains redirect URI and of the challenge. - void ApplyRedirect(GoogleApplyRedirectContext context); } } diff --git a/src/Microsoft.AspNet.Security.Google/project.json b/src/Microsoft.AspNet.Security.Google/project.json index bf9da22531..fbbb32c78a 100644 --- a/src/Microsoft.AspNet.Security.Google/project.json +++ b/src/Microsoft.AspNet.Security.Google/project.json @@ -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", diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs index 8f83dac3e4..825ab41216 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs @@ -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"; } } diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs index 6ee11c1d8f..502eaec13d 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs @@ -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 + internal class MicrosoftAccountAuthenticationHandler : OAuthAuthenticationHandler { - 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 InvokeAsync() + protected override async Task 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 AuthenticateCoreAsync() - { - AuthenticationProperties properties = null; - try - { - string code = null; - string state = null; - - IReadableStringCollection query = Request.Query; - IList 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>() - { - new KeyValuePair("client_id", Options.ClientId), - new KeyValuePair("redirect_uri", GenerateRedirectUri()), - new KeyValuePair("client_secret", Options.ClientSecret), - new KeyValuePair("code", code), - new KeyValuePair("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(); - - // Refresh token is only available when wl.offline_access is request. - // Otherwise, it is null. - var refreshToken = oauth2Token.Value("refresh_token"); - var expire = oauth2Token.Value("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 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); } } } diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs index 3f5e24cecf..29ad5c5efb 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs @@ -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 /// /// An ASP.NET middleware for authenticating users using the Microsoft Account service. /// - [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] - public class MicrosoftAccountAuthenticationMiddleware : AuthenticationMiddleware + public class MicrosoftAccountAuthenticationMiddleware : OAuthAuthenticationMiddleware { - private readonly ILogger _logger; - private readonly HttpClient _httpClient; - /// /// Initializes a new . /// @@ -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 } /// @@ -69,30 +48,7 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount /// An configured with the supplied to the constructor. protected override AuthenticationHandler 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); } } } diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs index 7668b0d085..24d2599cb2 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs @@ -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 { /// /// Configuration options for . /// - public class MicrosoftAccountAuthenticationOptions : AuthenticationOptions + public class MicrosoftAccountAuthenticationOptions : OAuthAuthenticationOptions { /// /// Initializes a new . @@ -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(); - BackchannelTimeout = TimeSpan.FromSeconds(60); + AuthorizationEndpoint = MicrosoftAccountAuthenticationDefaults.AuthorizationEndpoint; + TokenEndpoint = MicrosoftAccountAuthenticationDefaults.TokenEndpoint; + UserInformationEndpoint = MicrosoftAccountAuthenticationDefaults.UserInformationEndpoint; } -#if ASPNET50 - /// - /// Gets or sets the a pinned certificate validator to use to validate the endpoints used - /// in back channel communications belong to Microsoft Account. - /// - /// - /// The pinned certificate validator. - /// - /// 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. - public ICertificateValidator BackchannelCertificateValidator { get; set; } -#endif - /// - /// Get or sets the text that the user can display on a sign in user interface. - /// - /// - /// The default value is 'Microsoft'. - /// - public string Caption - { - get { return Description.Caption; } - set { Description.Caption = value; } - } - - /// - /// The application client ID assigned by the Microsoft authentication service. - /// - public string ClientId { get; set; } - - /// - /// The application client secret assigned by the Microsoft authentication service. - /// - public string ClientSecret { get; set; } - - /// - /// Gets or sets timeout value in milliseconds for back channel communications with Microsoft. - /// - /// - /// The back channel timeout. - /// - public TimeSpan BackchannelTimeout { get; set; } - - /// - /// 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. - /// - public HttpMessageHandler BackchannelHttpHandler { get; set; } - - /// - /// A list of permissions to request. - /// - public IList Scope { get; private set; } - - /// - /// 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". - /// - public PathString CallbackPath { get; set; } - - /// - /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user . - /// - public string SignInAsAuthenticationType { get; set; } - - /// - /// Gets or sets the used to handle authentication events. - /// - public IMicrosoftAccountAuthenticationNotifications Notifications { get; set; } - - /// - /// Gets or sets the type used to secure data handled by the middleware. - /// - public ISecureDataFormat StateDataFormat { get; set; } } } diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs index d7c43a4634..d6eb0f642b 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs @@ -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 { /// /// Specifies callback methods which the invokes to enable developer control over the authentication process. /// - public interface IMicrosoftAccountAuthenticationNotifications + public interface IMicrosoftAccountAuthenticationNotifications : IOAuthAuthenticationNotifications { /// /// Invoked whenever Microsoft succesfully authenticates a user. @@ -16,18 +17,5 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount /// Contains information about the login session as well as the user . /// A representing the completed operation. Task Authenticated(MicrosoftAccountAuthenticatedContext context); - - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// - /// A representing the completed operation. - Task ReturnEndpoint(MicrosoftAccountReturnEndpointContext context); - - /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft middleware. - /// - /// Contains redirect URI and of the challenge. - void ApplyRedirect(MicrosoftAccountApplyRedirectContext context); } } diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs index ef2b0b50f0..8b1a533123 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs @@ -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 /// /// Contains information about the login session as well as the user . /// - public class MicrosoftAccountAuthenticatedContext : BaseContext + public class MicrosoftAccountAuthenticatedContext : OAuthAuthenticatedContext { /// /// Initializes a new . /// /// The HTTP environment. /// The JSON-serialized user. - /// The access token provided by the Microsoft authentication service. - /// The refresh token provided by Microsoft authentication service. - /// Seconds until expiration. - public MicrosoftAccountAuthenticatedContext(HttpContext context, [NotNull] JObject user, string accessToken, - string refreshToken, string expires) - : base(context) + /// The access token provided by the Microsoft authentication service. + public MicrosoftAccountAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, [NotNull] JObject user, TokenResponse tokens) + : base(context, options, user, tokens) { IDictionary 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 } } - /// - /// Gets the JSON-serialized user. - /// - public JObject User { get; private set; } - - /// - /// Gets the access token provided by the Microsoft authenication service. - /// - public string AccessToken { get; private set; } - - /// - /// Gets the refresh token provided by Microsoft authentication service. - /// - /// - /// Refresh token is only available when wl.offline_access is request. - /// Otherwise, it is null. - /// - public string RefreshToken { get; private set; } - - /// - /// Gets the Microsoft access token expiration time. - /// - public TimeSpan? ExpiresIn { get; set; } - /// /// Gets the Microsoft Account user ID. /// @@ -112,16 +73,6 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount /// public string Email { get; private set; } - /// - /// Gets the representing the user. - /// - public ClaimsIdentity Identity { get; set; } - - /// - /// Gets or sets a property bag for common authentication properties. - /// - public AuthenticationProperties Properties { get; set; } - private static string PropertyValueIfExists(string property, IDictionary dictionary) { return dictionary.ContainsKey(property) ? dictionary[property].ToString() : null; diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs index deb561b941..26db09d15f 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs @@ -3,13 +3,14 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNet.Security.OAuth; namespace Microsoft.AspNet.Security.MicrosoftAccount { /// /// Default implementation. /// - public class MicrosoftAccountAuthenticationNotifications : IMicrosoftAccountAuthenticationNotifications + public class MicrosoftAccountAuthenticationNotifications : OAuthAuthenticationNotifications, IMicrosoftAccountAuthenticationNotifications { /// /// Initializes a new @@ -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); } /// @@ -26,16 +25,6 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount /// public Func OnAuthenticated { get; set; } - /// - /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. - /// - public Func OnReturnEndpoint { get; set; } - - /// - /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. - /// - public Action OnApplyRedirect { get; set; } - /// /// Invoked whenever Microsoft succesfully authenticates a user /// @@ -45,24 +34,5 @@ namespace Microsoft.AspNet.Security.MicrosoftAccount { return OnAuthenticated(context); } - - /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. - /// - /// Contains information about the login session as well as the user - /// A representing the completed operation. - public virtual Task ReturnEndpoint(MicrosoftAccountReturnEndpointContext context) - { - return OnReturnEndpoint(context); - } - - /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft account middleware. - /// - /// Contains redirect URI and of the challenge. - public virtual void ApplyRedirect(MicrosoftAccountApplyRedirectContext context) - { - OnApplyRedirect(context); - } } } diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json b/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json index 83bc4823bd..d452d0121d 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json @@ -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", diff --git a/src/Microsoft.AspNet.Security.OAuth/Microsoft.AspNet.Security.OAuth.kproj b/src/Microsoft.AspNet.Security.OAuth/Microsoft.AspNet.Security.OAuth.kproj new file mode 100644 index 0000000000..f0c8d7b472 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/Microsoft.AspNet.Security.OAuth.kproj @@ -0,0 +1,28 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 4a636011-68ee-4ce5-836d-ea8e13cf71e4 + Library + + + + ConsoleDebugger + + + WebDebugger + + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OAuth/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.OAuth/NotNullAttribute.cs new file mode 100644 index 0000000000..3f56c41518 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/NotNullAttribute.cs @@ -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 + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthAuthenticationNotifications.cs new file mode 100644 index 0000000000..b00b0175ea --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/Notifications/IOAuthAuthenticationNotifications.cs @@ -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 +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. + /// + public interface IOAuthAuthenticationNotifications + { + /// + /// 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. + /// + /// Contains information about the login session. + /// A representing the completed operation. + Task GetUserInformationAsync(OAuthGetUserInformationContext context); + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + Task ReturnEndpoint(OAuthReturnEndpointContext context); + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft middleware. + /// + /// Contains redirect URI and of the challenge. + void ApplyRedirect(OAuthApplyRedirectContext context); + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthApplyRedirectContext.cs similarity index 74% rename from src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountApplyRedirectContext.cs rename to src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthApplyRedirectContext.cs index f61a9eee43..df1f6a424b 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountApplyRedirectContext.cs +++ b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthApplyRedirectContext.cs @@ -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 { /// /// Context passed when a Challenge causes a redirect to authorize endpoint in the Microsoft account middleware. /// - public class MicrosoftAccountApplyRedirectContext : BaseContext + public class OAuthApplyRedirectContext : BaseContext { /// /// Creates a new context object. /// /// The HTTP request context. - /// The Microsoft account middleware options. /// The authentication properties of the challenge. /// The initial redirect URI. - 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; diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthAuthenticatedContext.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthAuthenticatedContext.cs new file mode 100644 index 0000000000..a651767e8e --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthAuthenticatedContext.cs @@ -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 +{ + /// + /// Contains information about the login session as well as the user . + /// + public class OAuthAuthenticatedContext : BaseContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The JSON-serialized user. + /// The tokens returned from the token endpoint. + 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); + } + } + + /// + /// Gets the JSON-serialized user. + /// + public JObject User { get; protected set; } + + /// + /// Gets the access token provided by the authentication service. + /// + public string AccessToken { get; protected set; } + + /// + /// Gets the access token type provided by the authentication service. + /// + public string TokenType { get; protected set; } + + /// + /// Gets the refresh token provided by the authentication service. + /// + public string RefreshToken { get; protected set; } + + /// + /// Gets the access token expiration time. + /// + public TimeSpan? ExpiresIn { get; protected set; } + + /// + /// Gets the representing the user. + /// + public ClaimsIdentity Identity { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties. + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthAuthenticationNotifications.cs new file mode 100644 index 0000000000..a91f8f0b0c --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthAuthenticationNotifications.cs @@ -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 +{ + /// + /// Default implementation. + /// + public class OAuthAuthenticationNotifications : IOAuthAuthenticationNotifications + { + /// + /// Initializes a new + /// + public OAuthAuthenticationNotifications() + { + OnGetUserInformationAsync = OAuthAuthenticationDefaults.DefaultOnGetUserInformationAsync; + OnReturnEndpoint = context => Task.FromResult(0); + OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnGetUserInformationAsync { get; set; } + + /// + /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. + /// + public Func OnReturnEndpoint { get; set; } + + /// + /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. + /// + public Action OnApplyRedirect { get; set; } + + /// + /// Invoked after the provider successfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task GetUserInformationAsync(OAuthGetUserInformationContext context) + { + return OnGetUserInformationAsync(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// Contains information about the login session as well as the user + /// A representing the completed operation. + public virtual Task ReturnEndpoint(OAuthReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the OAuth middleware. + /// + /// Contains redirect URI and of the challenge. + public virtual void ApplyRedirect(OAuthApplyRedirectContext context) + { + OnApplyRedirect(context); + } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthGetUserInformationContext.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthGetUserInformationContext.cs new file mode 100644 index 0000000000..5886d1b546 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthGetUserInformationContext.cs @@ -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 +{ + /// + /// Contains information about the login session as well as the user . + /// + public class OAuthGetUserInformationContext : BaseContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The JSON-serialized user. + /// The tokens returned from the token endpoint. + 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); + } + } + + /// + /// Gets the access token provided by the authentication service. + /// + public string AccessToken { get; protected set; } + + /// + /// Gets the access token type provided by the authentication service. + /// + public string TokenType { get; protected set; } + + /// + /// Gets the refresh token provided by the authentication service. + /// + public string RefreshToken { get; protected set; } + + /// + /// Gets the access token expiration time. + /// + public TimeSpan? ExpiresIn { get; protected set; } + + /// + /// Gets the backchannel used to communicate with the provider. + /// + public HttpClient Backchannel { get; protected set; } + + /// + /// Gets the representing the user. + /// + public ClaimsIdentity Identity { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties. + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountReturnEndpointContext.cs b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthReturnEndpointContext.cs similarity index 71% rename from src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountReturnEndpointContext.cs rename to src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthReturnEndpointContext.cs index 93e7670248..6f465ef3d2 100644 --- a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountReturnEndpointContext.cs +++ b/src/Microsoft.AspNet.Security.OAuth/Notifications/OAuthReturnEndpointContext.cs @@ -4,19 +4,19 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Security.Notifications; -namespace Microsoft.AspNet.Security.MicrosoftAccount +namespace Microsoft.AspNet.Security.OAuth { /// /// Provides context information to middleware providers. /// - public class MicrosoftAccountReturnEndpointContext : ReturnEndpointContext + public class OAuthReturnEndpointContext : ReturnEndpointContext { /// - /// Initializes a new . + /// Initializes a new . /// /// The HTTP environment. /// The authentication ticket. - public MicrosoftAccountReturnEndpointContext( + public OAuthReturnEndpointContext( HttpContext context, AuthenticationTicket ticket) : base(context, ticket) diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationDefaults.cs new file mode 100644 index 0000000000..758417ecfd --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationDefaults.cs @@ -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 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); + }; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationExtensions.cs new file mode 100644 index 0000000000..e140620e65 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationExtensions.cs @@ -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 +{ + /// + /// Extension methods for using + /// + public static class OAuthAuthenticationExtensions + { + /// + /// Authenticate users using OAuth. + /// + /// The passed to the configure method. + /// The middleware configuration options. + /// The updated . + public static IBuilder UseOAuthAuthentication([NotNull] this IBuilder app, [NotNull] OAuthAuthenticationOptions options) + { + if (string.IsNullOrEmpty(options.SignInAsAuthenticationType)) + { + options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + } + if (options.Notifications == null) + { + options.Notifications = new OAuthAuthenticationNotifications(); + } + return app.UseMiddleware, IOAuthAuthenticationNotifications>>(options); + } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationHandler.cs new file mode 100644 index 0000000000..eae1463638 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationHandler.cs @@ -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 : AuthenticationHandler + where TOptions : OAuthAuthenticationOptions + 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 InvokeAsync() + { + if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) + { + return await InvokeReturnPathAsync(); + } + return false; + } + + public async Task 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 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 ExchangeCodeAsync(string code, string redirectUri) + { + var tokenRequestParameters = new Dictionary() + { + { "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 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. + } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs new file mode 100644 index 0000000000..3dd00f4b0e --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationMiddleware.cs @@ -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 +{ + /// + /// An ASP.NET middleware for authenticating users using OAuth services. + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] + public class OAuthAuthenticationMiddleware : AuthenticationMiddleware + where TOptions : OAuthAuthenticationOptions + where TNotifications : IOAuthAuthenticationNotifications + { + /// + /// Initializes a new . + /// + /// The next middleware in the HTTP pipeline to invoke. + /// + /// + /// Configuration options for the middleware. + 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; } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new OAuthAuthenticationHandler(Backchannel, Logger); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(OAuthAuthenticationOptions 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; + } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationOptions.cs new file mode 100644 index 0000000000..69e8897448 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationOptions.cs @@ -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 +{ + /// + /// Configuration options for . + /// + public class OAuthAuthenticationOptions : AuthenticationOptions + { + /// + /// Initializes a new . + /// + public OAuthAuthenticationOptions([NotNull] string authenticationType) + : base(authenticationType) + { + Caption = authenticationType; + AuthenticationMode = AuthenticationMode.Passive; + Scope = new List(); + BackchannelTimeout = TimeSpan.FromSeconds(60); + } + + /// + /// Gets or sets the provider-assigned client id. + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the provider-assigned client secret. + /// + public string ClientSecret { get; set; } + + /// + /// Gets or sets the URI where the client will be redirected to authenticate. + /// + public string AuthorizationEndpoint { get; set; } + + /// + /// Gets or sets the URI the middleware will access to exchange the OAuth token. + /// + public string TokenEndpoint { get; set; } + + /// + /// 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. + /// + public string UserInformationEndpoint { get; set; } + +#if ASPNET50 + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to the auth provider. + /// + /// + /// The pinned certificate validator. + /// + /// 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. + public ICertificateValidator BackchannelCertificateValidator { get; set; } +#endif + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + /// + /// The default value is the authentication type. + /// + public string Caption + { + get { return Description.Caption; } + set { Description.Caption = value; } + } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with the auth provider. + /// + /// + /// The back channel timeout. + /// + public TimeSpan BackchannelTimeout { get; set; } + + /// + /// 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. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// A list of permissions to request. + /// + public IList Scope { get; private set; } + + /// + /// 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. + /// + public PathString CallbackPath { get; set; } + + /// + /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user . + /// + public string SignInAsAuthenticationType { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationOptions`1.cs b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationOptions`1.cs new file mode 100644 index 0000000000..65f01b5b82 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/OAuthAuthenticationOptions`1.cs @@ -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 +{ + /// + /// Configuration options for . + /// + public class OAuthAuthenticationOptions : OAuthAuthenticationOptions where TNotifications : IOAuthAuthenticationNotifications + { + /// + /// Initializes a new . + /// + public OAuthAuthenticationOptions([NotNull] string authenticationType) + : base(authenticationType) + { + } + + /// + /// Gets or sets the used to handle authentication events. + /// + public TNotifications Notifications { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuth/Resources.Designer.cs b/src/Microsoft.AspNet.Security.OAuth/Resources.Designer.cs new file mode 100644 index 0000000000..892b42b5fc --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/Resources.Designer.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Security.OAuth { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // 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() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [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; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided + { + get + { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. + /// + internal static string Exception_ValidatorHandlerMismatch { + get { + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.OAuth/Resources.resx b/src/Microsoft.AspNet.Security.OAuth/Resources.resx new file mode 100644 index 0000000000..2a19bea96a --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OAuth/TokenResponse.cs b/src/Microsoft.AspNet.Security.OAuth/TokenResponse.cs new file mode 100644 index 0000000000..84c3e95aa6 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/TokenResponse.cs @@ -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("access_token"); + TokenType = response.Value("token_type"); + RefreshToken = response.Value("refresh_token"); + ExpiresIn = response.Value("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; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.OAuth/project.json b/src/Microsoft.AspNet.Security.OAuth/project.json new file mode 100644 index 0000000000..b6ccd3fe50 --- /dev/null +++ b/src/Microsoft.AspNet.Security.OAuth/project.json @@ -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" + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs index b24aa71c30..7fbb4fbc3a 100644 --- a/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs @@ -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>() + var formPairs = new Dictionary() { - new KeyValuePair("oauth_verifier", verifier) + { "oauth_verifier", verifier }, }; request.Content = new FormUrlEncodedContent(formPairs); diff --git a/test/Microsoft.AspNet.Security.Test/Facebook/FacebookMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/Facebook/FacebookMiddlewareTests.cs index 8538948783..56467a7601 100644 --- a/test/Microsoft.AspNet.Security.Test/Facebook/FacebookMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Security.Test/Facebook/FacebookMiddlewareTests.cs @@ -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 configure, Func handler)