Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2283)

Side by Side Diff: services/authentication/google_authentication_impl.cc

Issue 1466733002: Google OAuth Device Flow support for FNL (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: Rebased to master Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 &param, 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 &param, 0, base::StringPrintf("%%%x", kEscapableUrlParamChars[i]),
63 std::string(1, kEscapableUrlParamChars[i]));
64 }
65 // Remove double-quotes, if present.
66 base::ReplaceChars(param, "\"", "", &param);
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698