diff --git a/Security.sln b/Security.sln
index 0d249fe568..5ae54786fc 100644
--- a/Security.sln
+++ b/Security.sln
@@ -26,6 +26,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.F
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SocialSample", "samples\SocialSample\SocialSample.kproj", "{8C73D216-332D-41D8-BFD0-45BC4BC36552}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.Google", "src\Microsoft.AspNet.Security.Google\Microsoft.AspNet.Security.Google.kproj", "{89BF8535-A849-458E-868A-A68FCF620486}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -96,6 +98,16 @@ Global
{8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x86.ActiveCfg = Release|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {89BF8535-A849-458E-868A-A68FCF620486}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -107,5 +119,6 @@ Global
{8DA26CD1-1302-4CFD-9270-9FA1B7C6138B} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
{3984651C-FD44-4394-8793-3D14EE348C04} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
{8C73D216-332D-41D8-BFD0-45BC4BC36552} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
+ {89BF8535-A849-458E-868A-A68FCF620486} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
EndGlobalSection
EndGlobal
diff --git a/samples/SocialSample/Project.json b/samples/SocialSample/Project.json
index f5434cb1c4..525d9dd6a6 100644
--- a/samples/SocialSample/Project.json
+++ b/samples/SocialSample/Project.json
@@ -6,6 +6,7 @@
"Microsoft.AspNet.Security": "1.0.0-*",
"Microsoft.AspNet.Security.Cookies": "1.0.0-*",
"Microsoft.AspNet.Security.Facebook": "1.0.0-*",
+ "Microsoft.AspNet.Security.Google": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
"Microsoft.Framework.DependencyInjection": "1.0.0-*"
},
diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs
index aec06926ea..68dd967e40 100644
--- a/samples/SocialSample/Startup.cs
+++ b/samples/SocialSample/Startup.cs
@@ -1,7 +1,9 @@
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Http.Security;
using Microsoft.AspNet.Security.Cookies;
using Microsoft.AspNet.Security.Facebook;
+using Microsoft.AspNet.Security.Google;
namespace CookieSample
{
@@ -15,6 +17,7 @@ namespace CookieSample
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
+ LoginPath = new PathString("/login"),
});
app.UseFacebookAuthentication(new FacebookAuthenticationOptions()
@@ -23,16 +26,75 @@ namespace CookieSample
AppSecret = "a124463c4719c94b4228d9a240e5dc1a",
});
- app.Run(async context =>
+ app.UseGoogleAuthentication(new GoogleAuthenticationOptions()
+ {
+ ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
+ ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f",
+ });
+
+ // Choose an authentication type
+ app.Map("/login", signoutApp =>
+ {
+ signoutApp.Run(async context =>
+ {
+ string authType = context.Request.Query["authtype"];
+ if (!string.IsNullOrEmpty(authType))
+ {
+ // By default the client will be redirect back to the URL that issued the challenge (/login?authtype=foo),
+ // send them to the home page instead (/).
+ context.Response.Challenge(new AuthenticationProperties() { RedirectUri = "/" }, authType);
+ return;
+ }
+
+ context.Response.ContentType = "text/html";
+ await context.Response.WriteAsync("
");
+ await context.Response.WriteAsync("Choose an authentication type:
");
+ foreach (var type in context.GetAuthenticationTypes())
+ {
+ await context.Response.WriteAsync("" + (type.Caption ?? "(suppressed)") + "
");
+ }
+ await context.Response.WriteAsync("");
+ });
+ });
+
+ // Sign-out to remove the user cookie.
+ app.Map("/logout", signoutApp =>
+ {
+ signoutApp.Run(async context =>
+ {
+ context.Response.SignOut(CookieAuthenticationDefaults.AuthenticationType);
+ context.Response.ContentType = "text/html";
+ await context.Response.WriteAsync("");
+ await context.Response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "
");
+ await context.Response.WriteAsync("Home");
+ await context.Response.WriteAsync("");
+ });
+ });
+
+ // Deny anonymous request beyond this point.
+ app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
- context.Response.Challenge("Facebook");
+ // The cookie middleware will intercept this 401 and redirect to /login
+ context.Response.Challenge();
return;
}
+ await next();
+ });
- context.Response.ContentType = "text/plain";
- await context.Response.WriteAsync("Hello " + context.User.Identity.Name);
+ // Display user information
+ app.Run(async context =>
+ {
+ context.Response.ContentType = "text/html";
+ await context.Response.WriteAsync("");
+ await context.Response.WriteAsync("Hello " + context.User.Identity.Name + "
");
+ foreach (var claim in context.User.Claims)
+ {
+ await context.Response.WriteAsync(claim.Type + ": " + claim.Value + "
");
+ }
+ await context.Response.WriteAsync("Logout");
+ await context.Response.WriteAsync("");
});
}
}
diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieApplyRedirectContext.cs
index 28cdf713c9..34907eabab 100644
--- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieApplyRedirectContext.cs
+++ b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieApplyRedirectContext.cs
@@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Security.Cookies
///
/// Creates a new context object.
///
- /// The OWIN request context
+ /// The HTTP request context
/// The cookie middleware options
/// The initial redirect URI
[SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#", Justification = "Represents header value")]
diff --git a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignInContext.cs b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignInContext.cs
index 598f985f54..28b47c6fe2 100644
--- a/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignInContext.cs
+++ b/src/Microsoft.AspNet.Security.Cookies/Notifications/CookieResponseSignInContext.cs
@@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Security.Cookies
///
/// Creates a new instance of the context object.
///
- /// The OWIN request context
+ /// The HTTP request context
/// The middleware options
/// Initializes AuthenticationType property
/// Initializes Identity property
diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs
index 895c7b08d8..5a3ef41e63 100644
--- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs
+++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs
@@ -26,12 +26,13 @@ namespace Microsoft.AspNet.Security.Facebook
/// Initializes a
///
/// The next middleware in the application pipeline to invoke
+ ///
+ ///
/// Configuration options for the middleware
public FacebookAuthenticationMiddleware(
RequestDelegate next,
IDataProtectionProvider dataProtectionProvider,
ILoggerFactory loggerFactory,
- IServiceProvider services,
FacebookAuthenticationOptions options)
: base(next, options)
{
diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationDefaults.cs
new file mode 100644
index 0000000000..b135063840
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationDefaults.cs
@@ -0,0 +1,10 @@
+// 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.Google
+{
+ public static class GoogleAuthenticationDefaults
+ {
+ public const string AuthenticationType = "Google";
+ }
+}
diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationExtensions.cs
new file mode 100644
index 0000000000..1203735352
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationExtensions.cs
@@ -0,0 +1,45 @@
+// 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.Security.Google;
+
+namespace Microsoft.AspNet.Builder
+{
+ ///
+ /// Extension methods for using
+ ///
+ public static class GoogleAuthenticationExtensions
+ {
+ ///
+ /// Authenticate users using Google OAuth 2.0
+ ///
+ /// The passed to the configure method
+ /// The google assigned client id
+ /// The google assigned client secret
+ /// The updated
+ public static IBuilder UseGoogleAuthentication([NotNull] this IBuilder app, [NotNull] string clientId, [NotNull] string clientSecret)
+ {
+ return app.UseGoogleAuthentication(
+ new GoogleAuthenticationOptions
+ {
+ ClientId = clientId,
+ ClientSecret = clientSecret
+ });
+ }
+
+ ///
+ /// Authenticate users using Google OAuth 2.0
+ ///
+ /// The passed to the configure method
+ /// Middleware configuration options
+ /// The updated
+ public static IBuilder UseGoogleAuthentication([NotNull] this IBuilder app, [NotNull] GoogleAuthenticationOptions options)
+ {
+ if (string.IsNullOrEmpty(options.SignInAsAuthenticationType))
+ {
+ options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType();
+ }
+ return app.UseMiddleware(options);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationHandler.cs
new file mode 100644
index 0000000000..fe2a4feab9
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationHandler.cs
@@ -0,0 +1,313 @@
+// 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.Google
+{
+ internal class GoogleAuthenticationHandler : AuthenticationHandler
+ {
+ 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)
+ {
+ _httpClient = httpClient;
+ _logger = logger;
+ }
+
+ 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);
+ }
+
+ 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);
+
+ 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);
+ AddQueryString(queryStrings, properties, "approval_prompt");
+ AddQueryString(queryStrings, properties, "login_hint");
+
+ 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;
+ }
+
+ private static void AddQueryString(IDictionary queryStrings, AuthenticationProperties properties,
+ string name, string defaultValue = null)
+ {
+ string value;
+ if (!properties.Dictionary.TryGetValue(name, out value))
+ {
+ value = defaultValue;
+ }
+ else
+ {
+ // Remove the parameter from AuthenticationProperties so it won't be serialized to state parameter
+ properties.Dictionary.Remove(name);
+ }
+
+ if (value == null)
+ {
+ return;
+ }
+
+ 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
new file mode 100644
index 0000000000..15c5c2a79c
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs
@@ -0,0 +1,98 @@
+// 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.Google
+{
+ ///
+ /// 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
+ {
+ private readonly ILogger _logger;
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// Initializes a
+ ///
+ /// The next middleware in the HTTP pipeline to invoke
+ ///
+ ///
+ /// Configuration options for the middleware
+ public GoogleAuthenticationMiddleware(
+ RequestDelegate next,
+ IDataProtectionProvider dataProtectionProvider,
+ ILoggerFactory loggerFactory,
+ GoogleAuthenticationOptions 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"));
+ }
+
+ _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
+ }
+
+ ///
+ /// Provides the object for processing authentication-related requests.
+ ///
+ /// 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 NET45
+ 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.Google/GoogleAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs
new file mode 100644
index 0000000000..fd7b069990
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs
@@ -0,0 +1,110 @@
+// 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 Microsoft.AspNet.Http;
+using Microsoft.AspNet.Http.Security;
+
+namespace Microsoft.AspNet.Security.Google
+{
+ ///
+ /// Configuration options for
+ ///
+ public class GoogleAuthenticationOptions : AuthenticationOptions
+ {
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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 NET45
+ ///
+ /// 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.
+ ///
+ public string AccessType { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNet.Security.Google/Microsoft.AspNet.Security.Google.kproj b/src/Microsoft.AspNet.Security.Google/Microsoft.AspNet.Security.Google.kproj
new file mode 100644
index 0000000000..111093c645
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/Microsoft.AspNet.Security.Google.kproj
@@ -0,0 +1,28 @@
+
+
+
+ 12.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 89bf8535-a849-458e-868a-a68fcf620486
+ Library
+
+
+
+ ConsoleDebugger
+
+
+ WebDebugger
+
+
+
+
+
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Security.Google/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.Google/NotNullAttribute.cs
new file mode 100644
index 0000000000..e87e361e4d
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/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.Google
+{
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
+ internal sealed class NotNullAttribute : Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs
new file mode 100644
index 0000000000..ac020b5639
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs
@@ -0,0 +1,43 @@
+// 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
+{
+ ///
+ /// 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 authenticaiton 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 authenticaiton 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
new file mode 100644
index 0000000000..ca89962769
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs
@@ -0,0 +1,158 @@
+// 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 Microsoft.AspNet.Http;
+using Microsoft.AspNet.Http.Security;
+using Microsoft.AspNet.Security.Notifications;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNet.Security.Google
+{
+ ///
+ /// Contains information about the login session as well as the user .
+ ///
+ public class GoogleAuthenticatedContext : BaseContext
+ {
+ ///
+ /// Initializes a
+ ///
+ /// 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)
+ {
+ 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");
+ FamilyName = TryGetValue(user, "name", "familyName");
+ Profile = TryGetValue(user, "url");
+ Email = TryGetFirstValue(user, "emails", "value");
+ }
+
+ ///
+ /// Gets the JSON-serialized user
+ ///
+ ///
+ /// Contains the Google user obtained from the endpoint https://www.googleapis.com/oauth2/v3/userinfo
+ ///
+ 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
+ ///
+ public string Id { get; private set; }
+
+ ///
+ /// Gets the user's name
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// Gets the user's given name
+ ///
+ public string GivenName { get; set; }
+
+ ///
+ /// Gets the user's family name
+ ///
+ public string FamilyName { get; set; }
+
+ ///
+ /// Gets the user's profile link
+ ///
+ public string Profile { get; private set; }
+
+ ///
+ /// Gets the user's email
+ ///
+ 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;
+ return user.TryGetValue(propertyName, out value) ? value.ToString() : null;
+ }
+
+ // Get the given subProperty from a property.
+ private static string TryGetValue(JObject user, string propertyName, string subProperty)
+ {
+ JToken value;
+ if (user.TryGetValue(propertyName, out value))
+ {
+ var subObject = JObject.Parse(value.ToString());
+ if (subObject != null && subObject.TryGetValue(subProperty, out value))
+ {
+ return value.ToString();
+ }
+ }
+ return null;
+ }
+
+ // Get the given subProperty from a list property.
+ private static string TryGetFirstValue(JObject user, string propertyName, string subProperty)
+ {
+ JToken value;
+ if (user.TryGetValue(propertyName, out value))
+ {
+ var array = JArray.Parse(value.ToString());
+ if (array != null && array.Count > 0)
+ {
+ var subObject = JObject.Parse(array.First.ToString());
+ if (subObject != null)
+ {
+ if (subObject.TryGetValue(subProperty, out value))
+ {
+ return value.ToString();
+ }
+ }
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs
new file mode 100644
index 0000000000..af93619ed4
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs
@@ -0,0 +1,69 @@
+// 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.Google
+{
+ ///
+ /// Default implementation.
+ ///
+ public class GoogleAuthenticationNotifications : IGoogleAuthenticationNotifications
+ {
+ ///
+ /// Initializes a
+ ///
+ public GoogleAuthenticationNotifications()
+ {
+ OnAuthenticated = context => Task.FromResult