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