From 9bb8b6114697f14a42e747acd1d1a41e6eed5af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 14 May 2015 16:03:21 +0200 Subject: [PATCH] Revisit OAuthAuthenticationHandler and add a new SaveTokensAsClaims option --- samples/CookieSample/Startup.cs | 6 +- samples/CookieSessionSample/Startup.cs | 8 +- samples/OpenIdConnectSample/Startup.cs | 5 +- samples/SocialSample/Startup.cs | 73 ++++++++------- .../FacebookAuthenticationHandler.cs | 82 +++++++++-------- .../FacebookAuthenticationHelper.cs | 40 ++++++++ .../FacebookAuthenticationMiddleware.cs | 7 +- .../FacebookAuthenticationOptions.cs | 4 +- .../FacebookAuthenticatedContext.cs | 62 ------------- .../FacebookAuthenticationNotifications.cs | 38 -------- .../IFacebookAuthenticationNotifications.cs | 21 ----- .../GoogleAuthenticationHandler.cs | 92 ++++++++++--------- ...ntext.cs => GoogleAuthenticationHelper.cs} | 48 ++-------- .../GoogleAuthenticationMiddleware.cs | 7 +- .../GoogleAuthenticationOptions.cs | 3 +- .../GoogleAuthenticationNotifications.cs | 38 -------- .../IGoogleAuthenticationNotifications.cs | 21 ----- .../MicrosoftAccountAuthenticationHandler.cs | 55 ++++++----- .../MicrosoftAccountAuthenticationHelper.cs | 41 +++++++++ ...icrosoftAccountAuthenticationMiddleware.cs | 6 +- .../MicrosoftAccountAuthenticationOptions.cs | 3 +- ...osoftAccountAuthenticationNotifications.cs | 21 ----- .../MicrosoftAccountAuthenticatedContext.cs | 81 ---------------- ...osoftAccountAuthenticationNotifications.cs | 38 -------- .../IOAuthAuthenticationNotifications.cs | 10 +- .../OAuthAuthenticatedContext.cs | 89 +++++++++++++----- .../OAuthAuthenticationNotifications.cs | 39 +++----- .../OAuthGetUserInformationContext.cs | 75 --------------- .../OAuthAuthenticationDefaults.cs | 39 -------- .../OAuthAuthenticationExtensions.cs | 10 +- .../OAuthAuthenticationHandler.cs | 58 +++++++++--- .../OAuthAuthenticationMiddleware.cs | 16 +--- .../OAuthAuthenticationOptions.cs | 31 ++++--- .../OAuthAuthenticationOptions`1.cs | 16 ---- ...TokenResponse.cs => OAuthTokenResponse.cs} | 4 +- .../TwitterAuthenticationHandler.cs | 55 +++++++---- .../TwitterAuthenticationOptions.cs | 11 ++- .../Facebook/FacebookMiddlewareTests.cs | 3 +- .../Google/GoogleMiddlewareTests.cs | 15 +-- .../MicrosoftAccountMiddlewareTests.cs | 5 +- 40 files changed, 494 insertions(+), 782 deletions(-) create mode 100644 src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHelper.cs delete mode 100644 src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticatedContext.cs delete mode 100644 src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticationNotifications.cs delete mode 100644 src/Microsoft.AspNet.Authentication.Facebook/Notifications/IFacebookAuthenticationNotifications.cs rename src/Microsoft.AspNet.Authentication.Google/{Notifications/GoogleAuthenticatedContext.cs => GoogleAuthenticationHelper.cs} (54%) delete mode 100644 src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticationNotifications.cs delete mode 100644 src/Microsoft.AspNet.Authentication.Google/Notifications/IGoogleAuthenticationNotifications.cs create mode 100644 src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHelper.cs delete mode 100644 src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs delete mode 100644 src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs delete mode 100644 src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs delete mode 100644 src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthGetUserInformationContext.cs delete mode 100644 src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationDefaults.cs delete mode 100644 src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions`1.cs rename src/Microsoft.AspNet.Authentication.OAuth/{TokenResponse.cs => OAuthTokenResponse.cs} (90%) diff --git a/samples/CookieSample/Startup.cs b/samples/CookieSample/Startup.cs index 04694b7cfd..6fd616bd7f 100644 --- a/samples/CookieSample/Startup.cs +++ b/samples/CookieSample/Startup.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Security.Claims; using Microsoft.AspNet.Authentication.Cookies; using Microsoft.AspNet.Builder; @@ -25,10 +26,11 @@ namespace CookieSample app.Run(async context => { - if (string.IsNullOrEmpty(context.User.Identity.Name)) + if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) { - var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") })); + var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") }, CookieAuthenticationDefaults.AuthenticationScheme)); await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, user); + context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Hello First timer"); return; diff --git a/samples/CookieSessionSample/Startup.cs b/samples/CookieSessionSample/Startup.cs index 5858c2c39f..1bfc635cc9 100644 --- a/samples/CookieSessionSample/Startup.cs +++ b/samples/CookieSessionSample/Startup.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Security.Claims; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; @@ -27,7 +28,7 @@ namespace CookieSessionSample app.Run(async context => { - if (string.IsNullOrEmpty(context.User.Identity.Name)) + if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) { // Make a large identity var claims = new List(1001); @@ -36,7 +37,10 @@ namespace CookieSessionSample { claims.Add(new Claim(ClaimTypes.Role, "SomeRandomGroup" + i, ClaimValueTypes.String, "IssuedByBob", "OriginalIssuerJoe")); } - await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(claims))); + + await context.Authentication.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme))); + context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Hello First timer"); return; diff --git a/samples/OpenIdConnectSample/Startup.cs b/samples/OpenIdConnectSample/Startup.cs index b07d87da50..3023a55aaa 100644 --- a/samples/OpenIdConnectSample/Startup.cs +++ b/samples/OpenIdConnectSample/Startup.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNet.Builder; +using System.Linq; +using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Authentication; @@ -38,7 +39,7 @@ namespace OpenIdConnectSample app.Run(async context => { - if (string.IsNullOrEmpty(context.User.Identity.Name)) + if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) { await context.Authentication.ChallengeAsync(OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" }); diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index 62d9c83803..bce45cb7a9 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; @@ -110,6 +111,7 @@ namespace CookieSample options.Caption = "MicrosoftAccount - Requires project changes"; options.ClientId = "00000000480FF62E"; options.ClientSecret = "bLw2JIvf8Y1TaToipPEqxTVlOeJwCUsr"; + options.Scope.Add("wl.emails"); }); // https://github.com/settings/applications/ @@ -131,48 +133,53 @@ namespace CookieSample options.TokenEndpoint = "https://github.com/login/oauth/access_token"; options.UserInformationEndpoint = "https://api.github.com/user"; options.ClaimsIssuer = "OAuth2-Github"; + options.SaveTokensAsClaims = false; // Retrieving user information is unique to each provider. - options.Notifications = new OAuthAuthenticationNotifications() + options.Notifications = new OAuthAuthenticationNotifications { - OnGetUserInformationAsync = async (context) => + OnAuthenticated = async notification => { // Get the GitHub user - var userRequest = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); - userRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); - userRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var userResponse = await context.Backchannel.SendAsync(userRequest, context.HttpContext.RequestAborted); - userResponse.EnsureSuccessStatusCode(); - var text = await userResponse.Content.ReadAsStringAsync(); - var user = JObject.Parse(text); + var request = new HttpRequestMessage(HttpMethod.Get, notification.Options.UserInformationEndpoint); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", notification.AccessToken); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var identity = new ClaimsIdentity( - context.Options.AuthenticationScheme, - ClaimsIdentity.DefaultNameClaimType, - ClaimsIdentity.DefaultRoleClaimType); + var response = await notification.Backchannel.SendAsync(request, notification.HttpContext.RequestAborted); + response.EnsureSuccessStatusCode(); - JToken value; - var id = user.TryGetValue("id", out value) ? value.ToString() : null; - if (!string.IsNullOrEmpty(id)) + var user = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var identifier = user.Value("id"); + if (!string.IsNullOrEmpty(identifier)) { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id, ClaimValueTypes.String, context.Options.ClaimsIssuer)); + notification.Identity.AddClaim(new Claim( + ClaimTypes.NameIdentifier, identifier, + ClaimValueTypes.String, notification.Options.ClaimsIssuer)); } - var userName = user.TryGetValue("login", out value) ? value.ToString() : null; + + var userName = user.Value("login"); if (!string.IsNullOrEmpty(userName)) { - identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, userName, ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } - 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.ClaimsIssuer)); - } - 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.ClaimsIssuer)); + notification.Identity.AddClaim(new Claim( + ClaimsIdentity.DefaultNameClaimType, userName, + ClaimValueTypes.String, notification.Options.ClaimsIssuer)); } - context.Principal = new ClaimsPrincipal(identity); + var name = user.Value("name"); + if (!string.IsNullOrEmpty(name)) + { + notification.Identity.AddClaim(new Claim( + "urn:github:name", name, + ClaimValueTypes.String, notification.Options.ClaimsIssuer)); + } + + var link = user.Value("url"); + if (!string.IsNullOrEmpty(link)) + { + notification.Identity.AddClaim(new Claim( + "urn:github:url", link, + ClaimValueTypes.String, notification.Options.ClaimsIssuer)); + } }, }; }); @@ -207,8 +214,8 @@ namespace CookieSample { signoutApp.Run(async context => { - await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); context.Response.ContentType = "text/html"; + await context.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await context.Response.WriteAsync(""); await context.Response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "
"); await context.Response.WriteAsync("Home"); @@ -219,7 +226,7 @@ namespace CookieSample // Deny anonymous request beyond this point. app.Use(async (context, next) => { - if (string.IsNullOrEmpty(context.User.Identity.Name)) + if (!context.User.Identities.Any(identity => identity.IsAuthenticated)) { // The cookie middleware will intercept this 401 and redirect to /login await context.Authentication.ChallengeAsync(); @@ -233,7 +240,7 @@ namespace CookieSample { context.Response.ContentType = "text/html"; await context.Response.WriteAsync(""); - await context.Response.WriteAsync("Hello " + context.User.Identity.Name + "
"); + await context.Response.WriteAsync("Hello " + (context.User.Identity.Name ?? "anonymous") + "
"); foreach (var claim in context.User.Claims) { await context.Response.WriteAsync(claim.Type + ": " + claim.Value + "
"); diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHandler.cs index 291956409e..cc9c1fdcf7 100644 --- a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHandler.cs @@ -17,14 +17,14 @@ using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.Facebook { - internal class FacebookAuthenticationHandler : OAuthAuthenticationHandler + internal class FacebookAuthenticationHandler : OAuthAuthenticationHandler { public FacebookAuthenticationHandler(HttpClient httpClient) : base(httpClient) { } - protected override async Task ExchangeCodeAsync(string code, string redirectUri) + protected override async Task ExchangeCodeAsync(string code, string redirectUri) { var queryBuilder = new QueryBuilder() { @@ -35,70 +35,78 @@ namespace Microsoft.AspNet.Authentication.Facebook { "client_secret", Options.AppSecret }, }; - var tokenResponse = await Backchannel.GetAsync(Options.TokenEndpoint + queryBuilder.ToString(), Context.RequestAborted); - tokenResponse.EnsureSuccessStatusCode(); - var oauthTokenResponse = await tokenResponse.Content.ReadAsStringAsync(); + var response = await Backchannel.GetAsync(Options.TokenEndpoint + queryBuilder.ToString(), Context.RequestAborted); + response.EnsureSuccessStatusCode(); - var form = new FormCollection(FormReader.ReadForm(oauthTokenResponse)); - var response = new JObject(); + var form = new FormCollection(FormReader.ReadForm(await response.Content.ReadAsStringAsync())); + var payload = new JObject(); foreach (string key in form.Keys) { - response.Add(string.Equals(key, "expires", StringComparison.OrdinalIgnoreCase) ? "expires_in" : key, form[key]); + payload.Add(string.Equals(key, "expires", StringComparison.OrdinalIgnoreCase) ? "expires_in" : key, form[key]); } + // The refresh token is not available. - return new TokenResponse(response); + return new OAuthTokenResponse(payload); } - protected override async Task GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens) + protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { - var graphAddress = Options.UserInformationEndpoint + "?access_token=" + UrlEncoder.UrlEncode(tokens.AccessToken); + var endpoint = Options.UserInformationEndpoint + "?access_token=" + UrlEncoder.UrlEncode(tokens.AccessToken); if (Options.SendAppSecretProof) { - graphAddress += "&appsecret_proof=" + GenerateAppSecretProof(tokens.AccessToken); + endpoint += "&appsecret_proof=" + GenerateAppSecretProof(tokens.AccessToken); } - var graphResponse = await Backchannel.GetAsync(graphAddress, Context.RequestAborted); - graphResponse.EnsureSuccessStatusCode(); - var text = await graphResponse.Content.ReadAsStringAsync(); - var user = JObject.Parse(text); + var response = await Backchannel.GetAsync(endpoint, Context.RequestAborted); + response.EnsureSuccessStatusCode(); - var context = new FacebookAuthenticatedContext(Context, Options, user, tokens); - var identity = new ClaimsIdentity( - Options.ClaimsIssuer, - ClaimsIdentity.DefaultNameClaimType, - ClaimsIdentity.DefaultRoleClaimType); - if (!string.IsNullOrEmpty(context.Id)) + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var notification = new OAuthAuthenticatedContext(Context, Options, Backchannel, tokens, payload) { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, ClaimValueTypes.String, Options.ClaimsIssuer)); + Properties = properties, + Principal = new ClaimsPrincipal(identity) + }; + + var identifier = FacebookAuthenticationHelper.GetId(payload); + if (!string.IsNullOrEmpty(identifier)) + { + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); } - if (!string.IsNullOrEmpty(context.UserName)) + + var userName = FacebookAuthenticationHelper.GetUserName(payload); + if (!string.IsNullOrEmpty(userName)) { - identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName, ClaimValueTypes.String, Options.ClaimsIssuer)); + identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, userName, ClaimValueTypes.String, Options.ClaimsIssuer)); } - if (!string.IsNullOrEmpty(context.Email)) + + var email = FacebookAuthenticationHelper.GetEmail(payload); + if (!string.IsNullOrEmpty(email)) { - identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, Options.ClaimsIssuer)); + identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); } - if (!string.IsNullOrEmpty(context.Name)) + + var name = FacebookAuthenticationHelper.GetName(payload); + if (!string.IsNullOrEmpty(name)) { - identity.AddClaim(new Claim("urn:facebook:name", context.Name, ClaimValueTypes.String, Options.ClaimsIssuer)); + identity.AddClaim(new Claim("urn:facebook:name", name, ClaimValueTypes.String, Options.ClaimsIssuer)); // Many Facebook accounts do not set the UserName field. Fall back to the Name field instead. - if (string.IsNullOrEmpty(context.UserName)) + if (string.IsNullOrEmpty(userName)) { - identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.Name, ClaimValueTypes.String, Options.ClaimsIssuer)); + identity.AddClaim(new Claim(identity.NameClaimType, name, ClaimValueTypes.String, Options.ClaimsIssuer)); } } - if (!string.IsNullOrEmpty(context.Link)) + + var link = FacebookAuthenticationHelper.GetLink(payload); + if (!string.IsNullOrEmpty(link)) { - identity.AddClaim(new Claim("urn:facebook:link", context.Link, ClaimValueTypes.String, Options.ClaimsIssuer)); + identity.AddClaim(new Claim("urn:facebook:link", link, ClaimValueTypes.String, Options.ClaimsIssuer)); } - context.Properties = properties; - context.Principal = new ClaimsPrincipal(identity); - await Options.Notifications.Authenticated(context); + await Options.Notifications.Authenticated(notification); - return new AuthenticationTicket(context.Principal, context.Properties, context.Options.AuthenticationScheme); + return new AuthenticationTicket(notification.Principal, notification.Properties, notification.Options.AuthenticationScheme); } private string GenerateAppSecretProof(string accessToken) diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHelper.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHelper.cs new file mode 100644 index 0000000000..3f9f98114f --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationHelper.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Internal; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.Facebook +{ + /// + /// Contains static methods that allow to extract user's information from a + /// instance retrieved from Facebook after a successful authentication process. + /// + public static class FacebookAuthenticationHelper + { + /// + /// Gets the Facebook user ID. + /// + public static string GetId([NotNull] JObject user) => user.Value("id"); + + /// + /// Gets the user's name. + /// + public static string GetName([NotNull] JObject user) => user.Value("name"); + + /// + /// Gets the user's link. + /// + public static string GetLink([NotNull] JObject user) => user.Value("link"); + + /// + /// Gets the Facebook username. + /// + public static string GetUserName([NotNull] JObject user) => user.Value("username"); + + /// + /// Gets the Facebook email. + /// + public static string GetEmail([NotNull] JObject user) => user.Value("email"); + } +} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs index 4eab19ddb8..1184f4479e 100644 --- a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationMiddleware.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Authentication.Facebook /// /// An ASP.NET middleware for authenticating users using Facebook. /// - public class FacebookAuthenticationMiddleware : OAuthAuthenticationMiddleware + public class FacebookAuthenticationMiddleware : OAuthAuthenticationMiddleware { /// /// Initializes a new . @@ -43,11 +43,6 @@ namespace Microsoft.AspNet.Authentication.Facebook { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AppSecret))); } - - if (Options.Notifications == null) - { - Options.Notifications = new FacebookAuthenticationNotifications(); - } } /// diff --git a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationOptions.cs index c7ba48aab4..4a5e1ce91d 100644 --- a/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.Facebook/FacebookAuthenticationOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Security.Claims; using Microsoft.AspNet.Http; using Microsoft.AspNet.Authentication.OAuth; @@ -9,7 +10,7 @@ namespace Microsoft.AspNet.Authentication.Facebook /// /// Configuration options for . /// - public class FacebookAuthenticationOptions : OAuthAuthenticationOptions + public class FacebookAuthenticationOptions : OAuthAuthenticationOptions { /// /// Initializes a new . @@ -23,6 +24,7 @@ namespace Microsoft.AspNet.Authentication.Facebook AuthorizationEndpoint = FacebookAuthenticationDefaults.AuthorizationEndpoint; TokenEndpoint = FacebookAuthenticationDefaults.TokenEndpoint; UserInformationEndpoint = FacebookAuthenticationDefaults.UserInformationEndpoint; + SaveTokensAsClaims = false; } // Facebook uses a non-standard term for this field. diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticatedContext.cs deleted file mode 100644 index 321b78f8b5..0000000000 --- a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticatedContext.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Authentication.OAuth; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNet.Authentication.Facebook -{ - /// - /// Contains information about the login session as well as the user . - /// - public class FacebookAuthenticatedContext : OAuthAuthenticatedContext - { - /// - /// Initializes a new . - /// - /// The HTTP environment. - /// The JSON-serialized user. - /// The Facebook Access token. - public FacebookAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user, TokenResponse tokens) - : base(context, options, user, tokens) - { - Id = TryGetValue(user, "id"); - Name = TryGetValue(user, "name"); - Link = TryGetValue(user, "link"); - UserName = TryGetValue(user, "username"); - Email = TryGetValue(user, "email"); - } - - /// - /// Gets the Facebook user ID. - /// - public string Id { get; private set; } - - /// - /// Gets the user's name. - /// - public string Name { get; private set; } - - /// - /// Gets the user's link. - /// - public string Link { get; private set; } - - /// - /// Gets the Facebook username. - /// - public string UserName { get; private set; } - - /// - /// Gets the Facebook email. - /// - public string Email { get; private set; } - - private static string TryGetValue(JObject user, string propertyName) - { - JToken value; - return user.TryGetValue(propertyName, out value) ? value.ToString() : null; - } - } -} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticationNotifications.cs deleted file mode 100644 index acee5b54e6..0000000000 --- a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/FacebookAuthenticationNotifications.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) .NET Foundation. 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; -using Microsoft.AspNet.Authentication.OAuth; - -namespace Microsoft.AspNet.Authentication.Facebook -{ - /// - /// The default implementation. - /// - public class FacebookAuthenticationNotifications : OAuthAuthenticationNotifications, IFacebookAuthenticationNotifications - { - /// - /// Initializes a new . - /// - public FacebookAuthenticationNotifications() - { - OnAuthenticated = context => Task.FromResult(null); - } - - /// - /// Gets or sets the function that is invoked when the Authenticated method is invoked. - /// - public Func OnAuthenticated { get; set; } - - /// - /// Invoked whenever Facebook succesfully authenticates a user. - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - public virtual Task Authenticated(FacebookAuthenticatedContext context) - { - return OnAuthenticated(context); - } - } -} diff --git a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/IFacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Facebook/Notifications/IFacebookAuthenticationNotifications.cs deleted file mode 100644 index aff7de6eeb..0000000000 --- a/src/Microsoft.AspNet.Authentication.Facebook/Notifications/IFacebookAuthenticationNotifications.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. 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; -using Microsoft.AspNet.Authentication.OAuth; - -namespace Microsoft.AspNet.Authentication.Facebook -{ - /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. - /// - public interface IFacebookAuthenticationNotifications : IOAuthAuthenticationNotifications - { - /// - /// Invoked when Facebook succesfully authenticates a user. - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - Task Authenticated(FacebookAuthenticatedContext context); - } -} diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHandler.cs index 621e77d830..986338fd1c 100644 --- a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHandler.cs @@ -14,65 +14,69 @@ using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.Google { - internal class GoogleAuthenticationHandler : OAuthAuthenticationHandler + internal class GoogleAuthenticationHandler : OAuthAuthenticationHandler { public GoogleAuthenticationHandler(HttpClient httpClient) : base(httpClient) { } - protected override async Task GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens) + protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { // Get the Google user var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); - var graphResponse = await Backchannel.SendAsync(request, Context.RequestAborted); - graphResponse.EnsureSuccessStatusCode(); - var text = await graphResponse.Content.ReadAsStringAsync(); - var user = JObject.Parse(text); - var context = new GoogleAuthenticatedContext(Context, Options, user, tokens); - var identity = new ClaimsIdentity( - Options.ClaimsIssuer, - ClaimsIdentity.DefaultNameClaimType, - ClaimsIdentity.DefaultRoleClaimType); + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + response.EnsureSuccessStatusCode(); - if (!string.IsNullOrEmpty(context.Id)) + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var notification = new OAuthAuthenticatedContext(Context, Options, Backchannel, tokens, payload) { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, - ClaimValueTypes.String, Options.ClaimsIssuer)); - } - if (!string.IsNullOrEmpty(context.GivenName)) - { - identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName, - ClaimValueTypes.String, Options.ClaimsIssuer)); - } - if (!string.IsNullOrEmpty(context.FamilyName)) - { - identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName, - ClaimValueTypes.String, Options.ClaimsIssuer)); - } - if (!string.IsNullOrEmpty(context.Name)) - { - identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String, - Options.ClaimsIssuer)); - } - if (!string.IsNullOrEmpty(context.Email)) - { - identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, - Options.ClaimsIssuer)); - } - if (!string.IsNullOrEmpty(context.Profile)) - { - identity.AddClaim(new Claim("urn:google:profile", context.Profile, ClaimValueTypes.String, - Options.ClaimsIssuer)); - } - context.Properties = properties; - context.Principal = new ClaimsPrincipal(identity); + Properties = properties, + Principal = new ClaimsPrincipal(identity) + }; - await Options.Notifications.Authenticated(context); + var identifier = GoogleAuthenticationHelper.GetId(payload); + if (!string.IsNullOrEmpty(identifier)) + { + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); + } - return new AuthenticationTicket(context.Principal, context.Properties, context.Options.AuthenticationScheme); + var givenName = GoogleAuthenticationHelper.GetGivenName(payload); + if (!string.IsNullOrEmpty(givenName)) + { + identity.AddClaim(new Claim(ClaimTypes.GivenName, givenName, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + var familyName = GoogleAuthenticationHelper.GetFamilyName(payload); + if (!string.IsNullOrEmpty(familyName)) + { + identity.AddClaim(new Claim(ClaimTypes.Surname, familyName, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + var name = GoogleAuthenticationHelper.GetName(payload); + if (!string.IsNullOrEmpty(name)) + { + identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + var email = GoogleAuthenticationHelper.GetEmail(payload); + if (!string.IsNullOrEmpty(email)) + { + identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + var profile = GoogleAuthenticationHelper.GetProfile(payload); + if (!string.IsNullOrEmpty(profile)) + { + identity.AddClaim(new Claim("urn:google:profile", profile, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + await Options.Notifications.Authenticated(notification); + + return new AuthenticationTicket(notification.Principal, notification.Properties, notification.Options.AuthenticationScheme); } // TODO: Abstract this properties override pattern into the base class? diff --git a/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHelper.cs similarity index 54% rename from src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticatedContext.cs rename to src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHelper.cs index 915b804361..611bfffa7b 100644 --- a/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticatedContext.cs +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationHelper.cs @@ -1,74 +1,46 @@ // Copyright (c) .NET Foundation. 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.Authentication; -using Microsoft.AspNet.Authentication.OAuth; +using Microsoft.Framework.Internal; using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.Google { /// - /// Contains information about the login session as well as the user . + /// Contains static methods that allow to extract user's information from a + /// instance retrieved from Google after a successful authentication process. /// - public class GoogleAuthenticatedContext : OAuthAuthenticatedContext + public static class GoogleAuthenticationHelper { - /// - /// Initializes a new . - /// - /// The HTTP environment. - /// The JSON-serialized Google user info. - /// Google OAuth 2.0 access token, refresh token, etc. - public GoogleAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user, TokenResponse tokens) - : base(context, options, user, tokens) - { - Id = TryGetValue(user, "id"); - Name = TryGetValue(user, "displayName"); - GivenName = TryGetValue(user, "name", "givenName"); - FamilyName = TryGetValue(user, "name", "familyName"); - Profile = TryGetValue(user, "url"); - Email = TryGetFirstValue(user, "emails", "value"); - } - /// /// Gets the Google user ID. /// - public string Id { get; private set; } + public static string GetId([NotNull] JObject user) => user.Value("id"); /// /// Gets the user's name. /// - public string Name { get; private set; } + public static string GetName([NotNull] JObject user) => user.Value("displayName"); /// /// Gets the user's given name. /// - public string GivenName { get; set; } + public static string GetGivenName([NotNull] JObject user) => TryGetValue(user, "name", "givenName"); /// /// Gets the user's family name. /// - public string FamilyName { get; set; } + public static string GetFamilyName([NotNull] JObject user) => TryGetValue(user, "name", "familyName"); /// /// Gets the user's profile link. /// - public string Profile { get; private set; } + public static string GetProfile([NotNull] JObject user) => user.Value("url"); /// /// Gets the user's email. /// - public string Email { get; private set; } - - private static string TryGetValue(JObject user, string propertyName) - { - JToken value; - return user.TryGetValue(propertyName, out value) ? value.ToString() : null; - } + public static string GetEmail([NotNull] JObject user) => TryGetFirstValue(user, "emails", "value"); // Get the given subProperty from a property. private static string TryGetValue(JObject user, string propertyName, string subProperty) diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs index db7a3c9881..3f07bc0e9e 100644 --- a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationMiddleware.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Authentication.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 : OAuthAuthenticationMiddleware + public class GoogleAuthenticationMiddleware : OAuthAuthenticationMiddleware { /// /// Initializes a new . @@ -35,11 +35,6 @@ namespace Microsoft.AspNet.Authentication.Google ConfigureOptions configureOptions = null) : base(next, dataProtectionProvider, loggerFactory, encoder, externalOptions, options, configureOptions) { - if (Options.Notifications == null) - { - Options.Notifications = new GoogleAuthenticationNotifications(); - } - if (Options.Scope.Count == 0) { // Google OAuth 2.0 asks for non-empty scope. If user didn't set it, set default scope to diff --git a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationOptions.cs index e65b800eda..f0c5d0f220 100644 --- a/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.Google/GoogleAuthenticationOptions.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Authentication.Google /// /// Configuration options for . /// - public class GoogleAuthenticationOptions : OAuthAuthenticationOptions + public class GoogleAuthenticationOptions : OAuthAuthenticationOptions { /// /// Initializes a new . @@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Authentication.Google AuthorizationEndpoint = GoogleAuthenticationDefaults.AuthorizationEndpoint; TokenEndpoint = GoogleAuthenticationDefaults.TokenEndpoint; UserInformationEndpoint = GoogleAuthenticationDefaults.UserInformationEndpoint; + SaveTokensAsClaims = false; } /// diff --git a/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticationNotifications.cs deleted file mode 100644 index 51269aeffa..0000000000 --- a/src/Microsoft.AspNet.Authentication.Google/Notifications/GoogleAuthenticationNotifications.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) .NET Foundation. 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; -using Microsoft.AspNet.Authentication.OAuth; - -namespace Microsoft.AspNet.Authentication.Google -{ - /// - /// The default implementation. - /// - public class GoogleAuthenticationNotifications : OAuthAuthenticationNotifications, IGoogleAuthenticationNotifications - { - /// - /// Initializes a new . - /// - public GoogleAuthenticationNotifications() - { - OnAuthenticated = context => Task.FromResult(null); - } - - /// - /// Gets or sets the function that is invoked when the Authenticated method is invoked. - /// - public Func OnAuthenticated { get; set; } - - /// - /// Invoked whenever Google succesfully authenticates a user. - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - public virtual Task Authenticated(GoogleAuthenticatedContext context) - { - return OnAuthenticated(context); - } - } -} diff --git a/src/Microsoft.AspNet.Authentication.Google/Notifications/IGoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.Google/Notifications/IGoogleAuthenticationNotifications.cs deleted file mode 100644 index 30ce697787..0000000000 --- a/src/Microsoft.AspNet.Authentication.Google/Notifications/IGoogleAuthenticationNotifications.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. 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; -using Microsoft.AspNet.Authentication.OAuth; - -namespace Microsoft.AspNet.Authentication.Google -{ - /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. - /// - public interface IGoogleAuthenticationNotifications : IOAuthAuthenticationNotifications - { - /// - /// Invoked whenever Google succesfully authenticates a user. - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - Task Authenticated(GoogleAuthenticatedContext context); - } -} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs index 31484f9985..0fac563eb8 100644 --- a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs @@ -11,45 +11,52 @@ using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.MicrosoftAccount { - internal class MicrosoftAccountAuthenticationHandler : OAuthAuthenticationHandler + internal class MicrosoftAccountAuthenticationHandler : OAuthAuthenticationHandler { public MicrosoftAccountAuthenticationHandler(HttpClient httpClient) : base(httpClient) { } - protected override async Task GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens) + protected override async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); - var graphResponse = await Backchannel.SendAsync(request, Context.RequestAborted); - graphResponse.EnsureSuccessStatusCode(); - var accountString = await graphResponse.Content.ReadAsStringAsync(); - var accountInformation = JObject.Parse(accountString); - var context = new MicrosoftAccountAuthenticatedContext(Context, Options, accountInformation, tokens); - context.Properties = properties; - var identity = new ClaimsIdentity( - new[] - { - new Claim(ClaimTypes.NameIdentifier, context.Id, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim("urn:microsoftaccount:id", context.Id, ClaimValueTypes.String, Options.ClaimsIssuer), - new Claim("urn:microsoftaccount:name", context.Name, ClaimValueTypes.String, Options.ClaimsIssuer) - }, - Options.ClaimsIssuer, - ClaimsIdentity.DefaultNameClaimType, - ClaimsIdentity.DefaultRoleClaimType); + var response = await Backchannel.SendAsync(request, Context.RequestAborted); + response.EnsureSuccessStatusCode(); - if (!string.IsNullOrWhiteSpace(context.Email)) + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var notification = new OAuthAuthenticatedContext(Context, Options, Backchannel, tokens, payload) { - identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, Options.ClaimsIssuer)); + Properties = properties, + Principal = new ClaimsPrincipal(identity) + }; + + var identifier = MicrosoftAccountAuthenticationHelper.GetId(payload); + if (!string.IsNullOrEmpty(identifier)) + { + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); + identity.AddClaim(new Claim("urn:microsoftaccount:id", identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); } - context.Principal = new ClaimsPrincipal(identity); - await Options.Notifications.Authenticated(context); + var name = MicrosoftAccountAuthenticationHelper.GetName(payload); + if (!string.IsNullOrEmpty(name)) + { + identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, Options.ClaimsIssuer)); + identity.AddClaim(new Claim("urn:microsoftaccount:name", name, ClaimValueTypes.String, Options.ClaimsIssuer)); + } - return new AuthenticationTicket(context.Principal, context.Properties, context.Options.AuthenticationScheme); + var email = MicrosoftAccountAuthenticationHelper.GetEmail(payload); + if (!string.IsNullOrWhiteSpace(email)) + { + identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + await Options.Notifications.Authenticated(notification); + + return new AuthenticationTicket(notification.Principal, notification.Properties, notification.Options.AuthenticationScheme); } } } diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHelper.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHelper.cs new file mode 100644 index 0000000000..e2629cdf38 --- /dev/null +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationHelper.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Internal; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Authentication.MicrosoftAccount +{ + /// + /// Contains static methods that allow to extract user's information from a + /// instance retrieved from Google after a successful authentication process. + /// + public static class MicrosoftAccountAuthenticationHelper + { + /// + /// Gets the Microsoft Account user ID. + /// + public static string GetId([NotNull] JObject user) => user.Value("id"); + + /// + /// Gets the user's name. + /// + public static string GetName([NotNull] JObject user) => user.Value("name"); + + /// + /// Gets the user's first name. + /// + public static string GetFirstName([NotNull] JObject user) => user.Value("first_name"); + + /// + /// Gets the user's last name. + /// + public static string GetLastName([NotNull] JObject user) => user.Value("last_name"); + + /// + /// Gets the user's email address. + /// + public static string GetEmail([NotNull] JObject user) => user.Value("emails") + ?.Value("preferred"); + } +} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs index 3626646294..e4f5751703 100644 --- a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNet.Authentication.MicrosoftAccount /// /// An ASP.NET middleware for authenticating users using the Microsoft Account service. /// - public class MicrosoftAccountAuthenticationMiddleware : OAuthAuthenticationMiddleware + public class MicrosoftAccountAuthenticationMiddleware : OAuthAuthenticationMiddleware { /// /// Initializes a new . @@ -33,10 +33,6 @@ namespace Microsoft.AspNet.Authentication.MicrosoftAccount ConfigureOptions configureOptions = null) : base(next, dataProtectionProvider, loggerFactory, encoder, externalOptions, options, configureOptions) { - if (Options.Notifications == null) - { - Options.Notifications = new MicrosoftAccountAuthenticationNotifications(); - } if (Options.Scope.Count == 0) { // LiveID requires a scope string, so if the user didn't set one we go for the least possible. diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs index 0212b6e7db..04d2bfa331 100644 --- a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Authentication.MicrosoftAccount /// /// Configuration options for . /// - public class MicrosoftAccountAuthenticationOptions : OAuthAuthenticationOptions + public class MicrosoftAccountAuthenticationOptions : OAuthAuthenticationOptions { /// /// Initializes a new . @@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Authentication.MicrosoftAccount AuthorizationEndpoint = MicrosoftAccountAuthenticationDefaults.AuthorizationEndpoint; TokenEndpoint = MicrosoftAccountAuthenticationDefaults.TokenEndpoint; UserInformationEndpoint = MicrosoftAccountAuthenticationDefaults.UserInformationEndpoint; + SaveTokensAsClaims = false; } } } diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs deleted file mode 100644 index d9647e4cdf..0000000000 --- a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) .NET Foundation. 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; -using Microsoft.AspNet.Authentication.OAuth; - -namespace Microsoft.AspNet.Authentication.MicrosoftAccount -{ - /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. - /// - public interface IMicrosoftAccountAuthenticationNotifications : IOAuthAuthenticationNotifications - { - /// - /// Invoked whenever Microsoft succesfully authenticates a user. - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - Task Authenticated(MicrosoftAccountAuthenticatedContext context); - } -} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs deleted file mode 100644 index c5bfa3f9bb..0000000000 --- a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Linq; -using Microsoft.AspNet.Authentication.OAuth; -using Microsoft.AspNet.Http; -using Microsoft.Framework.Internal; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNet.Authentication.MicrosoftAccount -{ - /// - /// Contains information about the login session as well as the user . - /// - public class MicrosoftAccountAuthenticatedContext : OAuthAuthenticatedContext - { - /// - /// Initializes a new . - /// - /// The HTTP environment. - /// The JSON-serialized user. - /// 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; - - JToken userId = User["id"]; - if (userId == null) - { - throw new ArgumentException(Resources.Exception_MissingId, nameof(user)); - } - - Id = userId.ToString(); - Name = PropertyValueIfExists("name", userAsDictionary); - FirstName = PropertyValueIfExists("first_name", userAsDictionary); - LastName = PropertyValueIfExists("last_name", userAsDictionary); - - if (userAsDictionary.ContainsKey("emails")) - { - JToken emailsNode = user["emails"]; - foreach (var childAsProperty in emailsNode.OfType().Where(childAsProperty => childAsProperty.Name == "preferred")) - { - Email = childAsProperty.Value.ToString(); - } - } - } - - /// - /// Gets the Microsoft Account user ID. - /// - public string Id { get; private set; } - - /// - /// Gets the user's name. - /// - public string Name { get; private set; } - - /// - /// Gets the user's first name. - /// - public string FirstName { get; private set; } - - /// - /// Gets the user's last name. - /// - public string LastName { get; private set; } - - /// - /// Gets the user's email address. - /// - public string Email { get; private set; } - - private static string PropertyValueIfExists(string property, IDictionary dictionary) - { - return dictionary.ContainsKey(property) ? dictionary[property].ToString() : null; - } - } -} diff --git a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs deleted file mode 100644 index 16491a4ed2..0000000000 --- a/src/Microsoft.AspNet.Authentication.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) .NET Foundation. 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; -using Microsoft.AspNet.Authentication.OAuth; - -namespace Microsoft.AspNet.Authentication.MicrosoftAccount -{ - /// - /// Default implementation. - /// - public class MicrosoftAccountAuthenticationNotifications : OAuthAuthenticationNotifications, IMicrosoftAccountAuthenticationNotifications - { - /// - /// Initializes a new - /// - public MicrosoftAccountAuthenticationNotifications() - { - OnAuthenticated = context => Task.FromResult(0); - } - - /// - /// Gets or sets the function that is invoked when the Authenticated method is invoked. - /// - public Func OnAuthenticated { get; set; } - - /// - /// Invoked whenever Microsoft succesfully authenticates a user - /// - /// Contains information about the login session as well as the user . - /// A representing the completed operation. - public virtual Task Authenticated(MicrosoftAccountAuthenticatedContext context) - { - return OnAuthenticated(context); - } - } -} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/IOAuthAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/IOAuthAuthenticationNotifications.cs index 6d386d089a..522f1cb162 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/IOAuthAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/IOAuthAuthenticationNotifications.cs @@ -1,7 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNet.Http.Authentication; namespace Microsoft.AspNet.Authentication.OAuth { @@ -12,21 +14,21 @@ namespace Microsoft.AspNet.Authentication.OAuth { /// /// 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. + /// This notification may not be invoked by sub-classes of OAuthAuthenticationHandler if they override CreateTicketAsync. /// /// Contains information about the login session. /// A representing the completed operation. - Task GetUserInformationAsync(OAuthGetUserInformationContext context); + Task Authenticated(OAuthAuthenticatedContext context); /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// 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. + /// Called when a Challenge causes a redirect to the authorize endpoint. /// /// Contains redirect URI and of the challenge. void ApplyRedirect(OAuthApplyRedirectContext context); diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticatedContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticatedContext.cs index d034117476..e59f04d07b 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticatedContext.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticatedContext.cs @@ -3,10 +3,12 @@ using System; using System.Globalization; +using System.Net.Http; using System.Security.Claims; +using Microsoft.AspNet.Authentication.Notifications; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; -using Microsoft.AspNet.Authentication.Notifications; +using Microsoft.Framework.Internal; using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.OAuth @@ -20,54 +22,97 @@ namespace Microsoft.AspNet.Authentication.OAuth /// Initializes a new . /// /// The HTTP environment. - /// The JSON-serialized user. + /// The options used by the authentication middleware. + /// The HTTP client used by the authentication middleware /// The tokens returned from the token endpoint. - public OAuthAuthenticatedContext(HttpContext context, OAuthAuthenticationOptions options, JObject user, - TokenResponse tokens) - : base(context, options) + public OAuthAuthenticatedContext( + [NotNull] HttpContext context, + [NotNull] OAuthAuthenticationOptions options, + [NotNull] HttpClient backchannel, + [NotNull] OAuthTokenResponse tokens) + : this(context, options, backchannel, tokens, user: new JObject()) { - 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. + /// Initializes a new . /// - public JObject User { get; protected set; } + /// The HTTP environment. + /// The options used by the authentication middleware. + /// The HTTP client used by the authentication middleware + /// The tokens returned from the token endpoint. + /// The JSON-serialized user. + public OAuthAuthenticatedContext( + [NotNull] HttpContext context, + [NotNull] OAuthAuthenticationOptions options, + [NotNull] HttpClient backchannel, + [NotNull] OAuthTokenResponse tokens, + [NotNull] JObject user) + : base(context, options) + { + TokenResponse = tokens; + Backchannel = backchannel; + } + + /// + /// Gets the JSON-serialized user or an empty + /// if it is not available. + /// + public JObject User { get; } + + /// + /// Gets the token response returned by the authentication service. + /// + public OAuthTokenResponse TokenResponse { get; } /// /// Gets the access token provided by the authentication service. /// - public string AccessToken { get; protected set; } + public string AccessToken => TokenResponse.AccessToken; /// /// Gets the access token type provided by the authentication service. /// - public string TokenType { get; protected set; } + public string TokenType => TokenResponse.TokenType; /// /// Gets the refresh token provided by the authentication service. /// - public string RefreshToken { get; protected set; } + public string RefreshToken => TokenResponse.RefreshToken; /// /// Gets the access token expiration time. /// - public TimeSpan? ExpiresIn { get; protected set; } + public TimeSpan? ExpiresIn + { + get + { + int value; + if (int.TryParse(TokenResponse.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value)) + { + return TimeSpan.FromSeconds(value); + } + + return null; + } + } + + /// + /// Gets the backchannel used to communicate with the provider. + /// + public HttpClient Backchannel { get; } /// - /// Gets the representing the user. + /// Gets the representing the user. /// public ClaimsPrincipal Principal { get; set; } + /// + /// Gets the main identity exposed by . + /// This property returns null when is null. + /// + public ClaimsIdentity Identity => Principal?.Identity as ClaimsIdentity; + /// /// Gets or sets a property bag for common authentication properties. /// diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticationNotifications.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticationNotifications.cs index aa6d7a8112..9716b14015 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthAuthenticationNotifications.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNet.Http.Authentication; namespace Microsoft.AspNet.Authentication.OAuth { @@ -11,58 +13,39 @@ namespace Microsoft.AspNet.Authentication.OAuth /// 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; } + public Func OnAuthenticated { get; set; } = context => Task.FromResult(0); /// /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. /// - public Func OnReturnEndpoint { get; set; } + public Func OnReturnEndpoint { get; set; } = context => Task.FromResult(0); /// /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. /// - public Action OnApplyRedirect { get; set; } + public Action OnApplyRedirect { get; set; } = context => context.Response.Redirect(context.RedirectUri); /// /// Invoked after the provider successfully authenticates a user. /// - /// Contains information about the login session as well as the 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); - } + public virtual Task Authenticated(OAuthAuthenticatedContext context) => OnAuthenticated(context); /// - /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// 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 + /// 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); - } + public virtual Task ReturnEndpoint(OAuthReturnEndpointContext context) => 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); - } + public virtual void ApplyRedirect(OAuthApplyRedirectContext context) => OnApplyRedirect(context); } } diff --git a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthGetUserInformationContext.cs b/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthGetUserInformationContext.cs deleted file mode 100644 index 74d02cd58c..0000000000 --- a/src/Microsoft.AspNet.Authentication.OAuth/Notifications/OAuthGetUserInformationContext.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Authentication; -using Microsoft.AspNet.Authentication.Notifications; - -namespace Microsoft.AspNet.Authentication.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 ClaimsPrincipal Principal { get; set; } - - /// - /// Gets or sets a property bag for common authentication properties. - /// - public AuthenticationProperties Properties { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationDefaults.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationDefaults.cs deleted file mode 100644 index a9b93d43a5..0000000000 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationDefaults.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) .NET Foundation. 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.Authentication.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.AuthenticationScheme, - ClaimsIdentity.DefaultNameClaimType, - ClaimsIdentity.DefaultRoleClaimType); - - identity.AddClaim(new Claim("access_token", context.AccessToken, ClaimValueTypes.String, context.Options.AuthenticationScheme)); - if (!string.IsNullOrEmpty(context.RefreshToken)) - { - identity.AddClaim(new Claim("refresh_token", context.RefreshToken, ClaimValueTypes.String, context.Options.AuthenticationScheme)); - } - if (!string.IsNullOrEmpty(context.TokenType)) - { - identity.AddClaim(new Claim("token_type", context.TokenType, ClaimValueTypes.String, context.Options.AuthenticationScheme)); - } - if (context.ExpiresIn.HasValue) - { - identity.AddClaim(new Claim("expires_in", context.ExpiresIn.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture), - ClaimValueTypes.String, context.Options.AuthenticationScheme)); - } - context.Principal = new ClaimsPrincipal(identity); - return Task.FromResult(0); - }; - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationExtensions.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationExtensions.cs index 3c6b3db9ae..fec82a735e 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationExtensions.cs @@ -19,10 +19,10 @@ namespace Microsoft.AspNet.Builder /// The passed to the configure method. /// The middleware configuration options. /// The updated . - public static IApplicationBuilder UseOAuthAuthentication([NotNull] this IApplicationBuilder app, [NotNull] string authenticationScheme, Action> configureOptions = null) + public static IApplicationBuilder UseOAuthAuthentication([NotNull] this IApplicationBuilder app, [NotNull] string authenticationScheme, Action configureOptions = null) { - return app.UseMiddleware, IOAuthAuthenticationNotifications>>( - new ConfigureOptions>(options => + return app.UseMiddleware>( + new ConfigureOptions(options => { options.AuthenticationScheme = authenticationScheme; options.Caption = authenticationScheme; @@ -30,10 +30,6 @@ namespace Microsoft.AspNet.Builder { configureOptions(options); } - if (options.Notifications == null) - { - options.Notifications = new OAuthAuthenticationNotifications(); - } }) { Name = authenticationScheme, diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs index 5d3a572e09..64dd51bc4c 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.DataHandler.Encoder; using Microsoft.AspNet.Http; @@ -19,9 +20,7 @@ using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.OAuth { - public class OAuthAuthenticationHandler : AuthenticationHandler - where TOptions : OAuthAuthenticationOptions - where TNotifications : IOAuthAuthenticationNotifications + public class OAuthAuthenticationHandler : AuthenticationHandler where TOptions : OAuthAuthenticationOptions { private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create(); @@ -124,7 +123,33 @@ namespace Microsoft.AspNet.Authentication.OAuth return new AuthenticationTicket(properties, Options.AuthenticationScheme); } - return await GetUserInformationAsync(properties, tokens); + var identity = new ClaimsIdentity(Options.ClaimsIssuer); + + if (Options.SaveTokensAsClaims) + { + identity.AddClaim(new Claim("access_token", tokens.AccessToken, + ClaimValueTypes.String, Options.ClaimsIssuer)); + + if (!string.IsNullOrEmpty(tokens.RefreshToken)) + { + identity.AddClaim(new Claim("refresh_token", tokens.RefreshToken, + ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + if (!string.IsNullOrEmpty(tokens.TokenType)) + { + identity.AddClaim(new Claim("token_type", tokens.TokenType, + ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + if (!string.IsNullOrEmpty(tokens.ExpiresIn)) + { + identity.AddClaim(new Claim("expires_in", tokens.ExpiresIn, + ClaimValueTypes.String, Options.ClaimsIssuer)); + } + } + + return await CreateTicketAsync(identity, properties, tokens); } catch (Exception ex) { @@ -133,7 +158,7 @@ namespace Microsoft.AspNet.Authentication.OAuth } } - protected virtual async Task ExchangeCodeAsync(string code, string redirectUri) + protected virtual async Task ExchangeCodeAsync(string code, string redirectUri) { var tokenRequestParameters = new Dictionary() { @@ -151,20 +176,27 @@ namespace Microsoft.AspNet.Authentication.OAuth requestMessage.Content = requestContent; var response = await Backchannel.SendAsync(requestMessage, Context.RequestAborted); response.EnsureSuccessStatusCode(); - var oauthTokenResponse = await response.Content.ReadAsStringAsync(); + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); - var oauth2Token = JObject.Parse(oauthTokenResponse); - return new TokenResponse(oauth2Token); + return new OAuthTokenResponse(payload); } - protected virtual async Task GetUserInformationAsync(AuthenticationProperties properties, TokenResponse tokens) + protected virtual async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) { - var context = new OAuthGetUserInformationContext(Context, Options, Backchannel, tokens) + var notification = new OAuthAuthenticatedContext(Context, Options, Backchannel, tokens) { - Properties = properties, + Principal = new ClaimsPrincipal(identity), + Properties = properties }; - await Options.Notifications.GetUserInformationAsync(context); - return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); + + await Options.Notifications.Authenticated(notification); + + if (notification.Principal?.Identity == null) + { + return null; + } + + return new AuthenticationTicket(notification.Principal, notification.Properties, Options.AuthenticationScheme); } protected override Task HandleUnauthorizedAsync([NotNull] ChallengeContext context) diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs index 67041821c3..c1c884810d 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationMiddleware.cs @@ -19,9 +19,7 @@ namespace Microsoft.AspNet.Authentication.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, new() - where TNotifications : IOAuthAuthenticationNotifications + public class OAuthAuthenticationMiddleware : AuthenticationMiddleware where TOptions : OAuthAuthenticationOptions, new() { /// /// Initializes a new . @@ -58,12 +56,12 @@ namespace Microsoft.AspNet.Authentication.OAuth if (string.IsNullOrWhiteSpace(Options.AuthorizationEndpoint)) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "AuthorizationEndpoint")); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.AuthorizationEndpoint))); } if (string.IsNullOrWhiteSpace(Options.TokenEndpoint)) { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "TokenEndpoint")); + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, nameof(Options.TokenEndpoint))); } if (Options.StateDataFormat == null) @@ -82,10 +80,6 @@ namespace Microsoft.AspNet.Authentication.OAuth { Options.SignInScheme = externalOptions.Options.SignInScheme; } - if (string.IsNullOrEmpty(Options.SignInScheme)) - { - throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "SignInScheme")); - } } protected HttpClient Backchannel { get; private set; } @@ -96,11 +90,11 @@ namespace Microsoft.AspNet.Authentication.OAuth /// An configured with the supplied to the constructor. protected override AuthenticationHandler CreateHandler() { - return new OAuthAuthenticationHandler(Backchannel); + return new OAuthAuthenticationHandler(Backchannel); } [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] - private static HttpMessageHandler ResolveHttpMessageHandler(OAuthAuthenticationOptions options) + private static HttpMessageHandler ResolveHttpMessageHandler(OAuthAuthenticationOptions options) { HttpMessageHandler handler = options.BackchannelHttpHandler ?? #if DNX451 diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions.cs index 62296c4a96..6244de1ef4 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Security.Claims; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; +using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Authentication.OAuth { @@ -14,15 +16,6 @@ namespace Microsoft.AspNet.Authentication.OAuth /// public class OAuthAuthenticationOptions : AuthenticationOptions { - /// - /// Initializes a new . - /// - public OAuthAuthenticationOptions() - { - Scope = new List(); - BackchannelTimeout = TimeSpan.FromSeconds(60); - } - /// /// Gets or sets the provider-assigned client id. /// @@ -46,7 +39,7 @@ namespace Microsoft.AspNet.Authentication.OAuth /// /// 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. + /// IOAuthAuthenticationNotifications.Authenticated or OAuthAuthenticationHandler.CreateTicketAsync. /// public string UserInformationEndpoint { get; set; } @@ -62,6 +55,7 @@ namespace Microsoft.AspNet.Authentication.OAuth /// 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. /// @@ -77,7 +71,7 @@ namespace Microsoft.AspNet.Authentication.OAuth /// /// The back channel timeout. /// - public TimeSpan BackchannelTimeout { get; set; } + public TimeSpan BackchannelTimeout { get; set; } = TimeSpan.FromSeconds(60); /// /// The HttpMessageHandler used to communicate with the auth provider. @@ -86,10 +80,15 @@ namespace Microsoft.AspNet.Authentication.OAuth /// public HttpMessageHandler BackchannelHttpHandler { get; set; } + /// + /// Gets or sets the used to handle authentication events. + /// + public IOAuthAuthenticationNotifications Notifications { get; [param: NotNull] set; } = new OAuthAuthenticationNotifications(); + /// /// A list of permissions to request. /// - public IList Scope { get; private set; } + public IList Scope { get; } = new List(); /// /// The request path within the application's base path where the user-agent will be returned. @@ -109,5 +108,13 @@ namespace Microsoft.AspNet.Authentication.OAuth /// Gets or sets the type used to secure data handled by the middleware. /// public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Defines whether access and refresh tokens should be stored in the + /// after a successful authentication. + /// You can set this property to false to reduce the size of the final + /// authentication cookie. Note that social providers set this property to false by default. + /// + public bool SaveTokensAsClaims { get; set; } = true; } } diff --git a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions`1.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions`1.cs deleted file mode 100644 index aa989dfe5c..0000000000 --- a/src/Microsoft.AspNet.Authentication.OAuth/OAuthAuthenticationOptions`1.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Authentication.OAuth -{ - /// - /// Configuration options for . - /// - public class OAuthAuthenticationOptions : OAuthAuthenticationOptions where TNotifications : IOAuthAuthenticationNotifications - { - /// - /// Gets or sets the used to handle authentication events. - /// - public TNotifications Notifications { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Authentication.OAuth/TokenResponse.cs b/src/Microsoft.AspNet.Authentication.OAuth/OAuthTokenResponse.cs similarity index 90% rename from src/Microsoft.AspNet.Authentication.OAuth/TokenResponse.cs rename to src/Microsoft.AspNet.Authentication.OAuth/OAuthTokenResponse.cs index 5bcb80ad8c..f796669553 100644 --- a/src/Microsoft.AspNet.Authentication.OAuth/TokenResponse.cs +++ b/src/Microsoft.AspNet.Authentication.OAuth/OAuthTokenResponse.cs @@ -5,9 +5,9 @@ using Newtonsoft.Json.Linq; namespace Microsoft.AspNet.Authentication.OAuth { - public class TokenResponse + public class OAuthTokenResponse { - public TokenResponse(JObject response) + public OAuthTokenResponse(JObject response) { Response = response; AccessToken = response.Value("access_token"); diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs index 201771f0c7..51229df9f8 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationHandler.cs @@ -82,24 +82,6 @@ namespace Microsoft.AspNet.Authentication.Twitter return new AuthenticationTicket(properties, Options.AuthenticationScheme); } - var accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier); - - var context = new TwitterAuthenticatedContext(Context, accessToken.UserId, accessToken.ScreenName, accessToken.Token, accessToken.TokenSecret); - - context.Principal = new ClaimsPrincipal( - new ClaimsIdentity( - new[] - { - new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, "http://www.w3.org/2001/XMLSchema#string", Options.ClaimsIssuer), - new Claim(ClaimTypes.Name, accessToken.ScreenName, "http://www.w3.org/2001/XMLSchema#string", Options.ClaimsIssuer), - new Claim("urn:twitter:userid", accessToken.UserId, "http://www.w3.org/2001/XMLSchema#string", Options.ClaimsIssuer), - new Claim("urn:twitter:screenname", accessToken.ScreenName, "http://www.w3.org/2001/XMLSchema#string", Options.ClaimsIssuer) - }, - Options.ClaimsIssuer, - ClaimsIdentity.DefaultNameClaimType, - ClaimsIdentity.DefaultRoleClaimType)); - context.Properties = requestToken.Properties; - var cookieOptions = new CookieOptions { HttpOnly = true, @@ -108,9 +90,23 @@ namespace Microsoft.AspNet.Authentication.Twitter Response.Cookies.Delete(StateCookie, cookieOptions); - await Options.Notifications.Authenticated(context); + var accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier); + + var identity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim(ClaimTypes.Name, accessToken.ScreenName, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim("urn:twitter:userid", accessToken.UserId, ClaimValueTypes.String, Options.ClaimsIssuer), + new Claim("urn:twitter:screenname", accessToken.ScreenName, ClaimValueTypes.String, Options.ClaimsIssuer) + }, + Options.ClaimsIssuer); - return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); + if (Options.SaveTokensAsClaims) + { + identity.AddClaim(new Claim("access_token", accessToken.Token, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + return await CreateTicketAsync(identity, properties, accessToken); } catch (Exception ex) { @@ -118,6 +114,25 @@ namespace Microsoft.AspNet.Authentication.Twitter return new AuthenticationTicket(properties, Options.AuthenticationScheme); } } + + protected virtual async Task CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token) + { + var notification = new TwitterAuthenticatedContext(Context, token.UserId, token.ScreenName, token.Token, token.TokenSecret) + { + Principal = new ClaimsPrincipal(identity), + Properties = properties + }; + + await Options.Notifications.Authenticated(notification); + + if (notification.Principal?.Identity == null) + { + return null; + } + + return new AuthenticationTicket(notification.Principal, notification.Properties, Options.AuthenticationScheme); + } + protected override async Task HandleUnauthorizedAsync([NotNull] ChallengeContext context) { var properties = new AuthenticationProperties(context.Properties); diff --git a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationOptions.cs b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationOptions.cs index 4768f59b50..73cac04563 100644 --- a/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Authentication.Twitter/TwitterAuthenticationOptions.cs @@ -3,6 +3,7 @@ using System; using System.Net.Http; +using System.Security.Claims; using Microsoft.AspNet.Http; using Microsoft.AspNet.Authentication.Twitter.Messages; @@ -102,8 +103,16 @@ namespace Microsoft.AspNet.Authentication.Twitter public ISecureDataFormat StateDataFormat { get; set; } /// - /// Gets or sets the used to handle authentication events. + /// Gets or sets the used to handle authentication events. /// public ITwitterAuthenticationNotifications Notifications { get; set; } + + /// + /// Defines whether access tokens should be stored in the + /// after a successful authentication. + /// This property is set to false by default to reduce + /// the size of the final authentication cookie. + /// + public bool SaveTokensAsClaims { get; set; } } } diff --git a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs index 8e4fe65225..711c601ba5 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Facebook/FacebookMiddlewareTests.cs @@ -4,6 +4,7 @@ using System; using System.Net; using System.Threading.Tasks; +using Microsoft.AspNet.Authentication.OAuth; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; @@ -33,7 +34,7 @@ namespace Microsoft.AspNet.Authentication.Facebook { options.AppId = "Test App Id"; options.AppSecret = "Test App Secret"; - options.Notifications = new FacebookAuthenticationNotifications + options.Notifications = new OAuthAuthenticationNotifications { OnApplyRedirect = context => { diff --git a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs index c48409eedf..ee55456dc4 100644 --- a/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/Google/GoogleMiddlewareTests.cs @@ -9,6 +9,7 @@ using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.DataHandler; +using Microsoft.AspNet.Authentication.OAuth; using Microsoft.AspNet.Builder; using Microsoft.AspNet.DataProtection; using Microsoft.AspNet.Http; @@ -198,7 +199,7 @@ namespace Microsoft.AspNet.Authentication.Google { options.ClientId = "Test Id"; options.ClientSecret = "Test Secret"; - options.Notifications = new GoogleAuthenticationNotifications + options.Notifications = new OAuthAuthenticationNotifications { OnApplyRedirect = context => { @@ -414,14 +415,14 @@ namespace Microsoft.AspNet.Authentication.Google return null; } }; - options.Notifications = new GoogleAuthenticationNotifications() + options.Notifications = new OAuthAuthenticationNotifications { OnAuthenticated = context => - { - var refreshToken = context.RefreshToken; - context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Google") }, "Google")); - return Task.FromResult(null); - } + { + var refreshToken = context.RefreshToken; + context.Principal.AddIdentity(new ClaimsIdentity(new Claim[] { new Claim("RefreshToken", refreshToken, ClaimValueTypes.String, "Google") }, "Google")); + return Task.FromResult(null); + } }; }); var properties = new AuthenticationProperties(); diff --git a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs index 253263fb54..42799750c7 100644 --- a/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs +++ b/test/Microsoft.AspNet.Authentication.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Authentication.DataHandler; using Microsoft.AspNet.Authentication.MicrosoftAccount; +using Microsoft.AspNet.Authentication.OAuth; using Microsoft.AspNet.Builder; using Microsoft.AspNet.DataProtection; using Microsoft.AspNet.Http; @@ -31,7 +32,7 @@ namespace Microsoft.AspNet.Authentication.Tests.MicrosoftAccount { options.ClientId = "Test Client Id"; options.ClientSecret = "Test Client Secret"; - options.Notifications = new MicrosoftAccountAuthenticationNotifications + options.Notifications = new OAuthAuthenticationNotifications { OnApplyRedirect = context => { @@ -143,7 +144,7 @@ namespace Microsoft.AspNet.Authentication.Tests.MicrosoftAccount return null; } }; - options.Notifications = new MicrosoftAccountAuthenticationNotifications + options.Notifications = new OAuthAuthenticationNotifications { OnAuthenticated = context => {