OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "services/authentication/google_authentication_impl.h" | |
6 | |
7 #include "base/json/json_reader.h" | |
8 #include "base/json/json_writer.h" | |
9 #include "base/memory/weak_ptr.h" | |
10 #include "base/strings/string_piece.h" | |
11 #include "base/strings/string_split.h" | |
12 #include "base/strings/string_tokenizer.h" | |
13 #include "base/strings/string_util.h" | |
14 #include "base/strings/stringprintf.h" | |
15 #include "base/synchronization/waitable_event.h" | |
16 #include "base/threading/platform_thread.h" | |
17 #include "base/trace_event/trace_event.h" | |
18 #include "base/values.h" | |
19 #include "mojo/common/binding_set.h" | |
20 #include "mojo/data_pipe_utils/data_pipe_drainer.h" | |
21 #include "mojo/data_pipe_utils/data_pipe_utils.h" | |
22 #include "mojo/public/c/system/main.h" | |
23 #include "mojo/public/cpp/bindings/strong_binding.h" | |
24 #include "mojo/public/cpp/system/macros.h" | |
25 #include "mojo/services/network/interfaces/url_loader.mojom.h" | |
26 #include "services/authentication/credentials_impl_db.mojom.h" | |
27 | |
28 namespace authentication { | |
29 | |
30 // Mojo Shell OAuth2 Client configuration. | |
31 // TODO: These should be retrieved from a secure storage or a configuration file | |
32 // in the future. | |
33 char kMojoShellOAuth2ClientId[] = | |
34 "962611923869-3avg0b4vlisgjhin0l98dgp6d8sd634r.apps.googleusercontent.com"; | |
35 char kMojoShellOAuth2ClientSecret[] = "41IxvPPAt1HyRoYw2hO84dRI"; | |
36 | |
37 // Query params used in Google OAuth2 handshake | |
38 char kOAuth2ClientIdParamName[] = "client_id"; | |
39 char kOAuth2ClientSecretParamName[] = "client_secret"; | |
40 char kOAuth2ScopeParamName[] = "scope"; | |
41 char kOAuth2GrantTypeParamName[] = "grant_type"; | |
42 char kOAuth2CodeParamName[] = "code"; | |
43 char kOAuth2RefreshTokenParamName[] = "refresh_token"; | |
44 char kOAuth2DeviceFlowGrantType[] = "http://oauth.net/grant_type/device/1.0"; | |
45 char kOAuth2RefreshTokenGrantType[] = "refresh_token"; | |
46 | |
47 // TODO(ukode) : Verify the char list | |
48 char kEscapableUrlParamChars[] = ".$[]/"; | |
49 | |
50 std::string EncodeParam(std::string param) { | |
51 for (size_t i = 0; i < strlen(kEscapableUrlParamChars); ++i) { | |
52 base::ReplaceSubstringsAfterOffset( | |
53 ¶m, 0, std::string(1, kEscapableUrlParamChars[i]), | |
54 base::StringPrintf("%%%x", kEscapableUrlParamChars[i])); | |
55 } | |
56 return param; | |
57 } | |
58 | |
59 std::string DecodeParam(std::string param) { | |
60 for (size_t i = 0; i < strlen(kEscapableUrlParamChars); ++i) { | |
61 base::ReplaceSubstringsAfterOffset( | |
62 ¶m, 0, base::StringPrintf("%%%x", kEscapableUrlParamChars[i]), | |
63 std::string(1, kEscapableUrlParamChars[i])); | |
64 } | |
65 // Remove double-quotes, if present. | |
66 base::ReplaceChars(param, "\"", "", ¶m); | |
67 return param; | |
68 } | |
69 | |
70 mojo::String BuildUrlQuery(mojo::Map<mojo::String, mojo::String> params) { | |
71 std::string message; | |
72 for (auto it = params.begin(); it != params.end(); ++it) { | |
73 message += EncodeParam(it.GetKey()) + "=" + EncodeParam(it.GetValue()); | |
74 message += "&"; | |
75 } | |
76 | |
77 if (!message.empty()) { | |
78 message = message.substr(0, message.size() - 1); // Trims extra "&". | |
79 } | |
80 return message; | |
81 } | |
82 | |
83 mojo::String ValueToString(const base::Value& value) { | |
84 if (value.IsType(base::Value::TYPE_STRING)) { | |
85 std::string value_string; | |
86 value.GetAsString(&value_string); | |
87 return value_string; | |
88 } | |
89 if (value.IsType(base::Value::TYPE_INTEGER)) { | |
90 int value_int; | |
91 value.GetAsInteger(&value_int); | |
92 return std::to_string(value_int); | |
93 } | |
94 if (value.IsType(base::Value::TYPE_BOOLEAN)) { | |
95 bool value_bool; | |
96 value.GetAsBoolean(&value_bool); | |
97 return value_bool ? "true" : "false"; | |
98 } | |
99 if (!value.IsType(base::Value::TYPE_NULL)) { | |
100 LOG(ERROR) << "Unexpected JSON value (requires string or null): " << value; | |
101 } | |
102 | |
103 return nullptr; | |
104 } | |
105 | |
106 scoped_ptr<base::DictionaryValue> ParseOAuth2Response( | |
107 const std::string& response) { | |
108 if (response.empty()) { | |
109 return nullptr; | |
110 } | |
111 | |
112 scoped_ptr<base::Value> root(base::JSONReader::Read(response)); | |
113 if (!root || !root->IsType(base::Value::TYPE_DICTIONARY)) { | |
114 LOG(ERROR) << "Unexpected json response:" << std::endl << response; | |
115 return nullptr; | |
116 } | |
117 | |
118 base::DictionaryValue::Iterator it( | |
119 *static_cast<base::DictionaryValue*>(root.get())); | |
120 scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue()); | |
121 std::string val; | |
122 while (!it.IsAtEnd()) { | |
123 val = ValueToString(it.value()); | |
124 dict->SetString(DecodeParam(it.key()), DecodeParam(val)); | |
qsr
2016/03/04 15:06:46
I do not understand why you need this. Why not kee
ukode
2016/03/11 22:48:52
Done. Agree, code looks a lot simpler. Earlier wit
| |
125 it.Advance(); | |
126 } | |
127 | |
128 return dict.Pass(); | |
129 } | |
130 | |
131 GoogleAuthenticationServiceImpl::GoogleAuthenticationServiceImpl( | |
132 mojo::InterfaceRequest<AuthenticationService> request, | |
133 const mojo::String app_url, | |
134 mojo::NetworkServicePtr& network_service, | |
135 mojo::files::DirectoryPtr& directory) | |
136 : binding_(this, request.Pass()), | |
137 app_url_(app_url), | |
138 network_service_(network_service) { | |
139 accounts_db_manager_ = | |
140 (new AccountsDbManager(directory.Pass()))->GetWeakPtr(); | |
141 } | |
142 | |
143 GoogleAuthenticationServiceImpl::~GoogleAuthenticationServiceImpl() {} | |
144 | |
145 void GoogleAuthenticationServiceImpl::GetOAuth2Token( | |
146 const mojo::String& username, | |
147 mojo::Array<mojo::String> scopes, | |
148 const GetOAuth2TokenCallback& callback) { | |
149 if (!accounts_db_manager_) { | |
150 callback.Run(nullptr, "Accounts db not available"); | |
151 return; | |
152 } | |
153 | |
154 authentication::CredentialsPtr creds = | |
155 accounts_db_manager_->GetCredentials(username); | |
156 | |
157 if (!creds->token) { | |
158 callback.Run(nullptr, "User grant not found"); | |
159 return; | |
160 } | |
161 | |
162 // TODO: Scopes are not used with the scoped refresh tokens. When we start | |
163 // supporting full login scoped tokens, then the scopes here gets used for | |
164 // Sidescoping. | |
165 mojo::Map<mojo::String, mojo::String> params; | |
166 params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId; | |
167 params[kOAuth2ClientSecretParamName] = kMojoShellOAuth2ClientSecret; | |
168 params[kOAuth2GrantTypeParamName] = kOAuth2RefreshTokenGrantType; | |
169 params[kOAuth2RefreshTokenParamName] = creds->token; | |
170 | |
171 Request("https://www.googleapis.com/oauth2/v3/token", "POST", | |
172 BuildUrlQuery(params.Pass()), | |
173 base::Bind(&GoogleAuthenticationServiceImpl::OnGetOAuth2Token, | |
174 base::Unretained(this), callback)); | |
175 } | |
176 | |
177 void GoogleAuthenticationServiceImpl::SelectAccount( | |
178 bool returnLastSelected, | |
179 const SelectAccountCallback& callback) { | |
180 if (!accounts_db_manager_) { | |
181 callback.Run(nullptr, "Accounts db not available"); | |
182 return; | |
183 } | |
184 | |
185 mojo::String username; | |
186 if (returnLastSelected) { | |
187 username = accounts_db_manager_->GetAuthorizedUserForApp(app_url_); | |
188 if (!username.is_null()) { | |
189 callback.Run(username, nullptr); | |
190 return; | |
191 } | |
192 } | |
193 | |
194 // TODO(ukode): Select one among the list of accounts using an AccountPicker | |
195 // UI instead of the first account always. | |
196 mojo::Array<mojo::String> users = accounts_db_manager_->GetAllUsers(); | |
197 if (!users.size()) { | |
198 callback.Run(nullptr, "No user accounts found."); | |
199 return; | |
200 } | |
201 | |
202 username = users[0]; | |
203 accounts_db_manager_->UpdateAuthorization(app_url_, username); | |
204 callback.Run(username, nullptr); | |
205 } | |
206 | |
207 void GoogleAuthenticationServiceImpl::ClearOAuth2Token( | |
208 const mojo::String& token) {} | |
209 | |
210 void GoogleAuthenticationServiceImpl::GetOAuth2DeviceCode( | |
211 mojo::Array<mojo::String> scopes, | |
212 const GetOAuth2DeviceCodeCallback& callback) { | |
213 std::string scopes_str("email"); | |
214 for (size_t i = 0; i < scopes.size(); i++) { | |
215 scopes_str += " "; | |
216 scopes_str += std::string(scopes[i].data()); | |
217 } | |
218 | |
219 mojo::Map<mojo::String, mojo::String> params; | |
220 params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId; | |
221 params[kOAuth2ScopeParamName] = scopes_str; | |
222 | |
223 Request("https://accounts.google.com/o/oauth2/device/code", "POST", | |
224 BuildUrlQuery(params.Pass()), | |
225 base::Bind(&GoogleAuthenticationServiceImpl::OnGetOAuth2DeviceCode, | |
226 base::Unretained(this), callback)); | |
227 } | |
228 | |
229 void GoogleAuthenticationServiceImpl::AddAccount( | |
230 const mojo::String& device_code, | |
231 const AddAccountCallback& callback) { | |
232 // Resets the poll count to "1" | |
233 AddAccount(device_code, 1, callback); | |
234 } | |
235 | |
236 void GoogleAuthenticationServiceImpl::AddAccount( | |
237 const mojo::String& device_code, | |
238 const uint32_t num_poll_attempts, | |
239 const AddAccountCallback& callback) { | |
240 mojo::Map<mojo::String, mojo::String> params; | |
241 params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId; | |
242 params[kOAuth2ClientSecretParamName] = kMojoShellOAuth2ClientSecret; | |
243 params[kOAuth2GrantTypeParamName] = kOAuth2DeviceFlowGrantType; | |
244 params[kOAuth2CodeParamName] = device_code; | |
245 | |
246 Request("https://www.googleapis.com/oauth2/v3/token", "POST", | |
247 BuildUrlQuery(params.Pass()), | |
248 base::Bind(&GoogleAuthenticationServiceImpl::OnAddAccount, | |
249 base::Unretained(this), callback, device_code, | |
250 num_poll_attempts)); | |
251 } | |
252 | |
253 void GoogleAuthenticationServiceImpl::OnGetOAuth2Token( | |
254 const GetOAuth2TokenCallback& callback, | |
255 const std::string& response, | |
256 const std::string& error) { | |
257 if (response.empty()) { | |
258 callback.Run(nullptr, "Error from server:" + error); | |
259 return; | |
260 } | |
261 | |
262 scoped_ptr<base::DictionaryValue> dict = | |
263 ParseOAuth2Response(response.c_str()); | |
264 if (!dict || dict->HasKey("error")) { | |
265 callback.Run(nullptr, "Error in parsing response:" + response); | |
266 return; | |
267 } | |
268 | |
269 std::string access_token; | |
270 dict->GetString("access_token", &access_token); | |
271 | |
272 callback.Run(access_token, nullptr); | |
273 } | |
274 | |
275 void GoogleAuthenticationServiceImpl::OnGetOAuth2DeviceCode( | |
276 const GetOAuth2DeviceCodeCallback& callback, | |
277 const std::string& response, | |
278 const std::string& error) { | |
279 if (response.empty()) { | |
280 callback.Run(nullptr, nullptr, nullptr, "Error from server:" + error); | |
281 return; | |
282 } | |
283 | |
284 scoped_ptr<base::DictionaryValue> dict = | |
285 ParseOAuth2Response(response.c_str()); | |
286 if (!dict || dict->HasKey("error")) { | |
287 callback.Run(nullptr, nullptr, nullptr, | |
288 "Error in parsing response:" + response); | |
289 return; | |
290 } | |
291 | |
292 std::string url; | |
293 std::string device_code; | |
294 std::string user_code; | |
295 dict->GetString("verification_url", &url); | |
296 dict->GetString("device_code", &device_code); | |
297 dict->GetString("user_code", &user_code); | |
298 | |
299 callback.Run(url, device_code, user_code, nullptr); | |
300 } | |
301 | |
302 void GoogleAuthenticationServiceImpl::GetTokenInfo( | |
303 const std::string& access_token) { | |
304 std::string url("https://www.googleapis.com/oauth2/v1/tokeninfo"); | |
305 url += "?access_token=" + EncodeParam(access_token); | |
306 | |
307 Request(url, "GET", "", | |
308 base::Bind(&GoogleAuthenticationServiceImpl::OnGetTokenInfo, | |
309 base::Unretained(this))); | |
310 } | |
311 | |
312 void GoogleAuthenticationServiceImpl::OnGetTokenInfo( | |
313 const std::string& response, | |
314 const std::string& error) { | |
315 if (response.empty()) { | |
316 return; | |
317 } | |
318 | |
319 scoped_ptr<base::DictionaryValue> dict = | |
320 ParseOAuth2Response(response.c_str()); | |
321 if (!dict || dict->HasKey("error")) { | |
322 return; | |
323 } | |
324 | |
325 // This field is only present if the profile scope was present in the | |
326 // request. The value of this field is an immutable identifier for the | |
327 // logged-in user, and may be used when creating and managing user | |
328 // sessions in your application. | |
329 dict->GetString("user_id", &user_id_); | |
330 dict->GetString("email", &email_); | |
331 // The space-delimited set of scopes that the user consented to. | |
332 dict->GetString("scope", &scope_); | |
333 return; | |
334 } | |
335 | |
336 void GoogleAuthenticationServiceImpl::GetUserInfo(const std::string& id_token) { | |
337 std::string url("https://www.googleapis.com/oauth2/v1/tokeninfo"); | |
338 url += "?id_token=" + EncodeParam(id_token); | |
339 | |
340 Request(url, "GET", "", | |
341 base::Bind(&GoogleAuthenticationServiceImpl::OnGetUserInfo, | |
342 base::Unretained(this))); | |
343 } | |
344 | |
345 void GoogleAuthenticationServiceImpl::OnGetUserInfo(const std::string& response, | |
346 const std::string& error) { | |
347 if (response.empty()) { | |
348 return; | |
349 } | |
350 | |
351 scoped_ptr<base::DictionaryValue> dict = | |
352 ParseOAuth2Response(response.c_str()); | |
353 if (!dict || dict->HasKey("error")) { | |
354 return; | |
355 } | |
356 | |
357 // This field is only present if the email scope was requested | |
358 dict->GetString("email", &email_); | |
359 | |
360 return; | |
361 } | |
362 | |
363 void GoogleAuthenticationServiceImpl::OnAddAccount( | |
364 const AddAccountCallback& callback, | |
365 const mojo::String& device_code, | |
366 const uint32_t num_poll_attempts, | |
367 const std::string& response, | |
368 const std::string& error) { | |
369 if (response.empty()) { | |
370 callback.Run(nullptr, "Error from server:" + error); | |
371 return; | |
372 } | |
373 | |
374 if (!response.empty() && error.empty()) { | |
375 scoped_ptr<base::Value> root(base::JSONReader::Read(response)); | |
376 if (!root || !root->IsType(base::Value::TYPE_DICTIONARY)) { | |
377 callback.Run(response, nullptr); | |
378 return; | |
379 } | |
380 } | |
381 | |
382 // Parse response and fetch refresh, access and idtokens | |
383 scoped_ptr<base::DictionaryValue> dict = | |
384 ParseOAuth2Response(response.c_str()); | |
385 std::string error_code; | |
386 if (!dict) { | |
387 callback.Run(nullptr, "Error in parsing response:" + response); | |
388 return; | |
389 } else if (dict->HasKey("error") && dict->GetString("error", &error_code)) { | |
390 if (error_code != "authorization_pending") { | |
391 callback.Run(nullptr, "Server error:" + response); | |
392 return; | |
393 } | |
394 | |
395 if (num_poll_attempts > 15) { | |
396 callback.Run(nullptr, "Timed out after max number of polling attempts"); | |
397 return; | |
398 } | |
399 | |
400 // Rate limit by waiting 7 seconds before polling for a new grant | |
401 base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(7000)); | |
qsr
2016/03/04 15:06:46
You are blocking your thread there. Can there be p
ukode
2016/03/11 22:48:52
Done. Moved it to using PostDelayedTask.
| |
402 AddAccount(device_code, num_poll_attempts + 1, | |
403 base::Bind(&GoogleAuthenticationServiceImpl::OnAddAccount, | |
404 base::Unretained(this), callback, device_code, | |
405 num_poll_attempts + 1)); | |
406 return; | |
407 } | |
408 | |
409 // Poll success, after detecting user grant. | |
410 std::string access_token; | |
411 dict->GetString("access_token", &access_token); | |
412 GetTokenInfo(access_token); // gets scope, email and user_id | |
413 | |
414 if (email_.empty()) { | |
415 std::string id_token; | |
416 dict->GetString("id_token", &id_token); | |
417 GetUserInfo(id_token); // gets user's email | |
418 } | |
419 | |
420 // TODO(ukode): Store access token in cache for the duration set in | |
421 // response | |
422 if (!accounts_db_manager_) { | |
423 callback.Run(nullptr, "Accounts db not available"); | |
424 return; | |
425 } | |
426 | |
427 std::string username = email_.empty() ? user_id_ : email_; | |
428 std::string refresh_token; | |
429 dict->GetString("refresh_token", &refresh_token); | |
430 authentication::CredentialsPtr creds = authentication::Credentials::New(); | |
431 creds->token = refresh_token; | |
432 creds->scopes = scope_; | |
433 creds->auth_provider = AuthProvider::GOOGLE; | |
434 creds->credential_type = CredentialType::DOWNSCOPED_OAUTH_REFRESH_TOKEN; | |
435 accounts_db_manager_->UpdateCredentials(username, creds.Pass()); | |
436 | |
437 callback.Run(username, nullptr); | |
438 } | |
439 | |
440 void GoogleAuthenticationServiceImpl::Request( | |
441 const std::string& url, | |
442 const std::string& method, | |
443 const std::string& message, | |
444 const GetOAuth2TokenCallback& callback) { | |
445 Request(url, method, message, callback, nullptr, 0); | |
446 } | |
447 | |
448 void GoogleAuthenticationServiceImpl::Request( | |
449 const std::string& url, | |
450 const std::string& method, | |
451 const std::string& message, | |
452 const GetOAuth2TokenCallback& callback, | |
453 const mojo::String& device_code, | |
454 const uint32_t num_poll_attempts) { | |
455 mojo::URLRequestPtr request(mojo::URLRequest::New()); | |
456 request->url = url; | |
457 request->method = method; | |
458 request->auto_follow_redirects = true; | |
459 | |
460 // Add headers | |
461 auto content_type_header = mojo::HttpHeader::New(); | |
462 content_type_header->name = "Content-Type"; | |
463 content_type_header->value = "application/x-www-form-urlencoded"; | |
464 request->headers.push_back(content_type_header.Pass()); | |
465 | |
466 if (!message.empty()) { | |
467 request->body.push_back( | |
468 mojo::common::WriteStringToConsumerHandle(message).Pass()); | |
469 } | |
470 | |
471 mojo::URLLoaderPtr url_loader; | |
472 network_service_->CreateURLLoader(GetProxy(&url_loader)); | |
473 | |
474 url_loader->Start( | |
475 request.Pass(), | |
476 base::Bind(&GoogleAuthenticationServiceImpl::HandleServerResponse, | |
477 base::Unretained(this), callback, device_code, | |
478 num_poll_attempts)); | |
479 | |
480 url_loader.WaitForIncomingResponse(); | |
481 } | |
482 | |
483 void GoogleAuthenticationServiceImpl::HandleServerResponse( | |
484 const GetOAuth2TokenCallback& callback, | |
485 const mojo::String& device_code, | |
486 const uint32_t num_poll_attempts, | |
487 mojo::URLResponsePtr response) { | |
488 if (response.is_null()) { | |
489 LOG(WARNING) << "Something went horribly wrong...exiting!!"; | |
490 callback.Run("", "Empty response"); | |
491 return; | |
492 } | |
493 | |
494 if (response->error) { | |
495 LOG(ERROR) << "Got error (" << response->error->code | |
496 << "), reason: " << response->error->description.get().c_str(); | |
497 callback.Run("", response->error->description.get().c_str()); | |
498 return; | |
499 } | |
500 | |
501 std::string response_body; | |
502 mojo::common::BlockingCopyToString(response->body.Pass(), &response_body); | |
503 | |
504 callback.Run(response_body, ""); | |
505 } | |
506 | |
507 } // authentication namespace | |
OLD | NEW |