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/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 |