Skip to content

Commit 8e6fedf

Browse files
authored
Merge pull request #254 from gwilymatgearset/add-google-signin-provider
Add Google Sign-In provider
2 parents 13bbd8a + 4400102 commit 8e6fedf

15 files changed

+1026
-1
lines changed

OwinOAuthProviders.sln

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Microsoft Visual Studio Solution File, Format Version 12.00
1+
Microsoft Visual Studio Solution File, Format Version 12.00
22
# Visual Studio 15
33
VisualStudioVersion = 15.0.26730.12
44
MinimumVisualStudioVersion = 10.0.40219.1
@@ -114,6 +114,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.Security.Providers.Arc
114114
EndProject
115115
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.Security.Providers.Typeform", "src\Owin.Security.Providers.Typeform\Owin.Security.Providers.Typeform.csproj", "{C8862B45-E1D1-4AB7-A83D-3A2FD2A22526}"
116116
EndProject
117+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.Security.Providers.Google", "src\Owin.Security.Providers.Google\Owin.Security.Providers.Google.csproj", "{ED434959-8CF8-4CAB-83B3-E4A618327AB5}"
118+
EndProject
117119
Global
118120
GlobalSection(SolutionConfigurationPlatforms) = preSolution
119121
Debug|Any CPU = Debug|Any CPU
@@ -344,6 +346,10 @@ Global
344346
{C8862B45-E1D1-4AB7-A83D-3A2FD2A22526}.Debug|Any CPU.Build.0 = Debug|Any CPU
345347
{C8862B45-E1D1-4AB7-A83D-3A2FD2A22526}.Release|Any CPU.ActiveCfg = Release|Any CPU
346348
{C8862B45-E1D1-4AB7-A83D-3A2FD2A22526}.Release|Any CPU.Build.0 = Release|Any CPU
349+
{ED434959-8CF8-4CAB-83B3-E4A618327AB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
350+
{ED434959-8CF8-4CAB-83B3-E4A618327AB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
351+
{ED434959-8CF8-4CAB-83B3-E4A618327AB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
352+
{ED434959-8CF8-4CAB-83B3-E4A618327AB5}.Release|Any CPU.Build.0 = Release|Any CPU
347353
EndGlobalSection
348354
GlobalSection(SolutionProperties) = preSolution
349355
HideSolutionNode = FALSE
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Owin.Security.Providers.Google
2+
{
3+
internal static class Constants
4+
{
5+
public const string DefaultAuthenticationType = "Google";
6+
}
7+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
3+
namespace Owin.Security.Providers.Google
4+
{
5+
public static class GoogleAuthenticationExtensions
6+
{
7+
public static IAppBuilder UseGoogleAuthentication(this IAppBuilder app,
8+
GoogleAuthenticationOptions options)
9+
{
10+
if (app == null)
11+
throw new ArgumentNullException(nameof(app));
12+
if (options == null)
13+
throw new ArgumentNullException(nameof(options));
14+
15+
app.Use(typeof(GoogleAuthenticationMiddleware), app, options);
16+
17+
return app;
18+
}
19+
20+
public static IAppBuilder UseGoogleAuthentication(this IAppBuilder app, string clientId, string clientSecret)
21+
{
22+
return app.UseGoogleAuthentication(new GoogleAuthenticationOptions
23+
{
24+
ClientId = clientId,
25+
ClientSecret = clientSecret
26+
});
27+
}
28+
}
29+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Security.Claims;
5+
using System.Threading.Tasks;
6+
using Microsoft.Owin.Infrastructure;
7+
using Microsoft.Owin.Logging;
8+
using Microsoft.Owin.Security;
9+
using Microsoft.Owin.Security.Infrastructure;
10+
using Newtonsoft.Json;
11+
using Newtonsoft.Json.Linq;
12+
using Owin.Security.Providers.Google.Provider;
13+
14+
namespace Owin.Security.Providers.Google
15+
{
16+
public class GoogleAuthenticationHandler : AuthenticationHandler<GoogleAuthenticationOptions>
17+
{
18+
private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";
19+
private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";
20+
// TODO: This url should come from here: https://accounts.google.com/.well-known/openid-configuration
21+
// TODO: as described by https://developers.google.com/identity/protocols/OpenIDConnect#discovery
22+
private const string UserInfoEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo";
23+
24+
private readonly ILogger _logger;
25+
private readonly HttpClient _httpClient;
26+
27+
public GoogleAuthenticationHandler(HttpClient httpClient, ILogger logger)
28+
{
29+
_httpClient = httpClient;
30+
_logger = logger;
31+
}
32+
33+
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
34+
{
35+
AuthenticationProperties properties = null;
36+
37+
try
38+
{
39+
string code = null;
40+
string state = null;
41+
42+
var query = Request.Query;
43+
var values = query.GetValues("code");
44+
if (values != null && values.Count == 1)
45+
{
46+
code = values[0];
47+
}
48+
values = query.GetValues("state");
49+
if (values != null && values.Count == 1)
50+
{
51+
state = values[0];
52+
}
53+
54+
properties = Options.StateDataFormat.Unprotect(state);
55+
if (properties == null)
56+
{
57+
return null;
58+
}
59+
60+
// OAuth2 10.12 CSRF
61+
if (!ValidateCorrelationId(properties, _logger))
62+
{
63+
return new AuthenticationTicket(null, properties);
64+
}
65+
66+
var requestPrefix = Request.Scheme + "://" + Request.Host;
67+
var redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath;
68+
69+
// Build up the body for the token request
70+
var body = new List<KeyValuePair<string, string>>
71+
{
72+
new KeyValuePair<string, string>("grant_type", "authorization_code"),
73+
new KeyValuePair<string, string>("code", code),
74+
new KeyValuePair<string, string>("redirect_uri", redirectUri),
75+
new KeyValuePair<string, string>("client_id", Options.ClientId),
76+
new KeyValuePair<string, string>("client_secret", Options.ClientSecret)
77+
};
78+
79+
// Request the token
80+
var tokenResponse =
81+
await _httpClient.PostAsync(TokenEndpoint, new FormUrlEncodedContent(body));
82+
tokenResponse.EnsureSuccessStatusCode();
83+
var text = await tokenResponse.Content.ReadAsStringAsync();
84+
85+
// Deserializes the token response
86+
dynamic response = JsonConvert.DeserializeObject<dynamic>(text);
87+
var accessToken = (string)response.access_token;
88+
var expires = (string) response.expires_in;
89+
string refreshToken = null;
90+
if (response.refresh_token != null)
91+
refreshToken = (string) response.refresh_token;
92+
93+
// Get the Google user
94+
var graphResponse = await _httpClient.GetAsync(
95+
UserInfoEndpoint + "?access_token=" + Uri.EscapeDataString(accessToken), Request.CallCancelled);
96+
graphResponse.EnsureSuccessStatusCode();
97+
text = await graphResponse.Content.ReadAsStringAsync();
98+
var userInfo = JObject.Parse(text);
99+
100+
var context = new GoogleAuthenticatedContext(Context, userInfo, accessToken, expires, refreshToken)
101+
{
102+
Identity = new ClaimsIdentity(
103+
Options.AuthenticationType,
104+
ClaimsIdentity.DefaultNameClaimType,
105+
ClaimsIdentity.DefaultRoleClaimType)
106+
};
107+
if (!string.IsNullOrEmpty(context.Id))
108+
{
109+
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType));
110+
}
111+
if (!string.IsNullOrEmpty(context.UserName))
112+
{
113+
context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName, XmlSchemaString, Options.AuthenticationType));
114+
}
115+
if (!string.IsNullOrEmpty(context.Email))
116+
{
117+
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType));
118+
}
119+
if (!string.IsNullOrEmpty(context.Name))
120+
{
121+
context.Identity.AddClaim(new Claim("urn:google:name", context.Name, XmlSchemaString, Options.AuthenticationType));
122+
}
123+
if (!string.IsNullOrEmpty(context.Link))
124+
{
125+
context.Identity.AddClaim(new Claim("urn:google:url", context.Link, XmlSchemaString, Options.AuthenticationType));
126+
}
127+
context.Properties = properties;
128+
129+
await Options.Provider.Authenticated(context);
130+
131+
return new AuthenticationTicket(context.Identity, context.Properties);
132+
}
133+
catch (Exception ex)
134+
{
135+
_logger.WriteError(ex.Message);
136+
}
137+
return new AuthenticationTicket(null, properties);
138+
}
139+
140+
protected override Task ApplyResponseChallengeAsync()
141+
{
142+
if (Response.StatusCode != 401)
143+
{
144+
return Task.FromResult<object>(null);
145+
}
146+
147+
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
148+
149+
if (challenge == null) return Task.FromResult<object>(null);
150+
var baseUri =
151+
Request.Scheme +
152+
Uri.SchemeDelimiter +
153+
Request.Host +
154+
Request.PathBase;
155+
156+
var currentUri =
157+
baseUri +
158+
Request.Path +
159+
Request.QueryString;
160+
161+
var redirectUri =
162+
baseUri +
163+
Options.CallbackPath;
164+
165+
var properties = challenge.Properties;
166+
if (string.IsNullOrEmpty(properties.RedirectUri))
167+
{
168+
properties.RedirectUri = currentUri;
169+
}
170+
171+
// OAuth2 10.12 CSRF
172+
GenerateCorrelationId(properties);
173+
174+
// comma separated
175+
var scope = string.Join(" ", Options.Scope);
176+
177+
var state = Options.StateDataFormat.Protect(properties);
178+
179+
var authorizationEndpoint =
180+
"https://accounts.google.com/o/oauth2/auth" +
181+
"?response_type=code" +
182+
"&client_id=" + Uri.EscapeDataString(Options.ClientId) +
183+
"&redirect_uri=" + Uri.EscapeDataString(redirectUri) +
184+
"&scope=" + Uri.EscapeDataString(scope) +
185+
"&state=" + Uri.EscapeDataString(state);
186+
187+
// Check if offline access was requested
188+
if (Options.RequestOfflineAccess)
189+
authorizationEndpoint += "&access_type=offline";
190+
191+
Response.Redirect(authorizationEndpoint);
192+
193+
return Task.FromResult<object>(null);
194+
}
195+
196+
public override async Task<bool> InvokeAsync()
197+
{
198+
return await InvokeReplyPathAsync();
199+
}
200+
201+
private async Task<bool> InvokeReplyPathAsync()
202+
{
203+
if (!Options.CallbackPath.HasValue || Options.CallbackPath != Request.Path) return false;
204+
// TODO: error responses
205+
206+
var ticket = await AuthenticateAsync();
207+
if (ticket == null)
208+
{
209+
_logger.WriteWarning("Invalid return state, unable to redirect.");
210+
Response.StatusCode = 500;
211+
return true;
212+
}
213+
214+
var context = new GoogleReturnEndpointContext(Context, ticket)
215+
{
216+
SignInAsAuthenticationType = Options.SignInAsAuthenticationType,
217+
RedirectUri = ticket.Properties.RedirectUri
218+
};
219+
220+
await Options.Provider.ReturnEndpoint(context);
221+
222+
if (context.SignInAsAuthenticationType != null &&
223+
context.Identity != null)
224+
{
225+
var grantIdentity = context.Identity;
226+
if (!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal))
227+
{
228+
grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType);
229+
}
230+
Context.Authentication.SignIn(context.Properties, grantIdentity);
231+
}
232+
233+
if (context.IsRequestCompleted || context.RedirectUri == null) return context.IsRequestCompleted;
234+
var redirectUri = context.RedirectUri;
235+
if (context.Identity == null)
236+
{
237+
// add a redirect hint that sign-in failed in some way
238+
redirectUri = WebUtilities.AddQueryString(redirectUri, "error", "access_denied");
239+
}
240+
Response.Redirect(redirectUri);
241+
context.RequestCompleted();
242+
243+
return context.IsRequestCompleted;
244+
}
245+
}
246+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Net.Http;
4+
using Microsoft.Owin;
5+
using Microsoft.Owin.Logging;
6+
using Microsoft.Owin.Security;
7+
using Microsoft.Owin.Security.DataHandler;
8+
using Microsoft.Owin.Security.DataProtection;
9+
using Microsoft.Owin.Security.Infrastructure;
10+
using Owin.Security.Providers.Google.Provider;
11+
12+
namespace Owin.Security.Providers.Google
13+
{
14+
public class GoogleAuthenticationMiddleware : AuthenticationMiddleware<GoogleAuthenticationOptions>
15+
{
16+
private readonly HttpClient _httpClient;
17+
private readonly ILogger _logger;
18+
19+
public GoogleAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app,
20+
GoogleAuthenticationOptions options)
21+
: base(next, options)
22+
{
23+
if (string.IsNullOrWhiteSpace(Options.ClientId))
24+
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
25+
Resources.Exception_OptionMustBeProvided, "ClientId"));
26+
if (string.IsNullOrWhiteSpace(Options.ClientSecret))
27+
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
28+
Resources.Exception_OptionMustBeProvided, "ClientSecret"));
29+
30+
_logger = app.CreateLogger<GoogleAuthenticationMiddleware>();
31+
32+
if (Options.Provider == null)
33+
Options.Provider = new GoogleAuthenticationProvider();
34+
35+
if (Options.StateDataFormat == null)
36+
{
37+
var dataProtector = app.CreateDataProtector(
38+
typeof (GoogleAuthenticationMiddleware).FullName,
39+
Options.AuthenticationType, "v1");
40+
Options.StateDataFormat = new PropertiesDataFormat(dataProtector);
41+
}
42+
43+
if (string.IsNullOrEmpty(Options.SignInAsAuthenticationType))
44+
Options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType();
45+
46+
_httpClient = new HttpClient(ResolveHttpMessageHandler(Options))
47+
{
48+
Timeout = Options.BackchannelTimeout,
49+
MaxResponseContentBufferSize = 1024*1024*10
50+
};
51+
}
52+
53+
/// <summary>
54+
/// Provides the <see cref="T:Microsoft.Owin.Security.Infrastructure.AuthenticationHandler" /> object for processing
55+
/// authentication-related requests.
56+
/// </summary>
57+
/// <returns>
58+
/// An <see cref="T:Microsoft.Owin.Security.Infrastructure.AuthenticationHandler" /> configured with the
59+
/// <see cref="T:Owin.Security.Providers.GooglePlus.GooglePlusAuthenticationOptions" /> supplied to the constructor.
60+
/// </returns>
61+
protected override AuthenticationHandler<GoogleAuthenticationOptions> CreateHandler()
62+
{
63+
return new GoogleAuthenticationHandler(_httpClient, _logger);
64+
}
65+
66+
private static HttpMessageHandler ResolveHttpMessageHandler(GoogleAuthenticationOptions options)
67+
{
68+
var handler = options.BackchannelHttpHandler ?? new WebRequestHandler();
69+
70+
// If they provided a validator, apply it or fail.
71+
if (options.BackchannelCertificateValidator == null) return handler;
72+
// Set the cert validate callback
73+
var webRequestHandler = handler as WebRequestHandler;
74+
if (webRequestHandler == null)
75+
{
76+
throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch);
77+
}
78+
webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate;
79+
80+
return handler;
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)