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
+ }
0 commit comments