Chromium Code Reviews| Index: services/authentication/google_authentication_impl.cc |
| diff --git a/services/authentication/google_authentication_impl.cc b/services/authentication/google_authentication_impl.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..307a997cb446f9bc0999b202baae3cd9aea8326a |
| --- /dev/null |
| +++ b/services/authentication/google_authentication_impl.cc |
| @@ -0,0 +1,490 @@ |
| +// Copyright 2015 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +#include "services/authentication/google_authentication_impl.h" |
| + |
| +#include "base/json/json_reader.h" |
| +#include "base/json/json_writer.h" |
| +#include "base/memory/weak_ptr.h" |
| +#include "base/strings/string_piece.h" |
| +#include "base/strings/string_split.h" |
| +#include "base/strings/string_tokenizer.h" |
| +#include "base/strings/string_util.h" |
| +#include "base/synchronization/waitable_event.h" |
| +#include "base/threading/platform_thread.h" |
| +#include "base/trace_event/trace_event.h" |
| +#include "base/values.h" |
| +#include "mojo/common/binding_set.h" |
| +#include "mojo/data_pipe_utils/data_pipe_drainer.h" |
| +#include "mojo/data_pipe_utils/data_pipe_utils.h" |
| +#include "mojo/public/c/system/main.h" |
| +#include "mojo/public/cpp/bindings/strong_binding.h" |
| +#include "mojo/public/cpp/system/macros.h" |
| +#include "mojo/services/network/interfaces/url_loader.mojom.h" |
| +#include "services/authentication/auth_data.h" |
| + |
| +namespace authentication { |
| + |
| +// Mojo Shell OAuth2 Client configuration. |
| +// TODO: These should be retrieved from a secure storage or a configuration file |
| +// in the future. |
| +char kMojoShellOAuth2ClientId[] = |
| + "962611923869-3avg0b4vlisgjhin0l98dgp6d8sd634r.apps.googleusercontent.com"; |
| +char kMojoShellOAuth2ClientSecret[] = "41IxvPPAt1HyRoYw2hO84dRI"; |
| + |
| +// Query params used in Google OAuth2 handshake |
| +const std::string kKeyValSeparator("="); |
| +char kUrlQueryParamSeparator[] = "&"; |
| + |
| +char kOAuth2ClientIdParamName[] = "client_id"; |
| +char kOAuth2ClientSecretParamName[] = "client_secret"; |
| +char kOAuth2ScopeParamName[] = "scope"; |
| +char kOAuth2GrantTypeParamName[] = "grant_type"; |
| +char kOAuth2CodeParamName[] = "code"; |
| +char kOAuth2RefreshTokenParamName[] = "refresh_token"; |
| +char kOAuth2DeviceFlowGrantType[] = "http://oauth.net/grant_type/device/1.0"; |
| +char kOAuth2RefreshTokenGrantType[] = "refresh_token"; |
| + |
| +mojo::String ValueToString(const base::Value& value) { |
| + if (value.IsType(base::Value::TYPE_STRING)) { |
| + std::string value_string; |
| + value.GetAsString(&value_string); |
| + return value_string; |
| + } |
| + if (value.IsType(base::Value::TYPE_INTEGER)) { |
| + int value_int; |
| + value.GetAsInteger(&value_int); |
| + return std::to_string(value_int); |
| + } |
| + if (value.IsType(base::Value::TYPE_BOOLEAN)) { |
| + bool value_bool; |
| + value.GetAsBoolean(&value_bool); |
| + return value_bool ? "true" : "false"; |
| + } |
| + if (!value.IsType(base::Value::TYPE_NULL)) { |
| + LOG(ERROR) << "Unexpected JSON value (requires string or null): " << value; |
| + } |
| + |
| + return nullptr; |
| +} |
| + |
| +scoped_ptr<base::DictionaryValue> ParseOAuth2Response( |
| + const std::string& response) { |
| + if (response.empty()) { |
| + return nullptr; |
| + } |
| + |
| + scoped_ptr<base::Value> root(base::JSONReader::Read(response)); |
| + if (!root || !root->IsType(base::Value::TYPE_DICTIONARY)) { |
| + LOG(ERROR) << "Unexpected json response:" << std::endl << response; |
| + return nullptr; |
| + } |
| + |
| + base::DictionaryValue::Iterator it( |
| + *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
|
| + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue()); |
| + std::string val; |
| + while (!it.IsAtEnd()) { |
| + val = ValueToString(it.value()); |
| + 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
|
| + dict->SetString(it.key(), val); |
| + it.Advance(); |
| + } |
| + |
| + return dict.Pass(); |
| +} |
| + |
| +GoogleAuthenticationServiceImpl::GoogleAuthenticationServiceImpl( |
| + mojo::InterfaceRequest<AuthenticationService> request, |
| + const mojo::String app_url, |
| + mojo::NetworkServicePtr& network_service, |
| + mojo::files::DirectoryPtr& directory) |
| + : binding_(this, request.Pass()), |
| + app_url_(app_url), |
| + network_service_(network_service), |
| + weak_ptr_factory_(this) { |
| + 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.
|
| +} |
| + |
| +GoogleAuthenticationServiceImpl::~GoogleAuthenticationServiceImpl() {} |
| + |
| +void GoogleAuthenticationServiceImpl::GetOAuth2Token( |
| + const mojo::String& username, |
| + mojo::Array<mojo::String> scopes, |
| + const GetOAuth2TokenCallback& callback) { |
| + mojo::String user_data = |
| + accounts_db_manager_->GetAccountDataForUser(username); |
| + AuthData* auth_data = authentication::GetAuthDataFromString(user_data.get()); |
| + |
| + if ((auth_data == nullptr) || auth_data->persistent_credential.empty()) { |
| + callback.Run(nullptr, "User grant not found"); |
| + return; |
| + } |
| + |
| + // TODO: Scopes are not used with the scoped refresh tokens. When we start |
| + // supporting full login scoped tokens, then the scopes here gets used for |
| + // Sidescoping. |
| + std::string message; |
| + message += |
| + kOAuth2ClientIdParamName + kKeyValSeparator + kMojoShellOAuth2ClientId; |
| + |
| + 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.
|
| + message += kOAuth2ClientSecretParamName + kKeyValSeparator + |
| + kMojoShellOAuth2ClientSecret; |
| + |
| + message += kUrlQueryParamSeparator; |
| + message += kOAuth2GrantTypeParamName + kKeyValSeparator + |
| + kOAuth2RefreshTokenGrantType; |
| + |
| + message += kUrlQueryParamSeparator; |
| + message += kOAuth2RefreshTokenParamName + kKeyValSeparator; |
| + message += auth_data->persistent_credential; |
| + |
| + Request("https://www.googleapis.com/oauth2/v3/token", "POST", message, |
| + base::Bind(&GoogleAuthenticationServiceImpl::OnGetOAuth2Token, |
| + base::Unretained(this), callback)); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::SelectAccount( |
| + bool returnLastSelected, |
| + const SelectAccountCallback& callback) { |
| + mojo::String username; |
| + if (returnLastSelected) { |
| + username = accounts_db_manager_->GetAuthorizedUserForApp(app_url_); |
| + if (!username.is_null()) { |
| + callback.Run(username, nullptr); |
| + return; |
| + } |
| + } |
| + |
| + // TODO(ukode): Select one among the list of accounts using an AccountPicker |
| + // UI instead of the first account always. |
| + mojo::String user_list = accounts_db_manager_->GetAllUserAccounts(); |
| + if (user_list.is_null()) { |
| + callback.Run(nullptr, "No user accounts found."); |
| + return; |
| + } |
| + |
| + base::StringTokenizer lines(user_list, "\n"); |
| + std::string entry; |
| + if (!lines.GetNext()) { |
| + callback.Run(nullptr, "No valid user accounts found."); |
| + return; |
| + } |
| + entry = lines.token(); |
| + AuthData* auth_data = authentication::GetAuthDataFromString(entry); |
| + username = auth_data->username; |
| + |
| + accounts_db_manager_->UpdateAuthorization(app_url_, username); |
| + callback.Run(username, nullptr); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::ClearOAuth2Token( |
| + const mojo::String& token) {} |
| + |
| +void GoogleAuthenticationServiceImpl::GetOAuth2DeviceCode( |
| + mojo::Array<mojo::String> scopes, |
| + const GetOAuth2DeviceCodeCallback& callback) { |
| + std::string message; |
| + message += |
| + kOAuth2ClientIdParamName + kKeyValSeparator + kMojoShellOAuth2ClientId; |
| + |
| + std::string scopes_str("email"); |
| + for (size_t i = 0; i < scopes.size(); i++) { |
| + scopes_str += " "; |
| + scopes_str += std::string(scopes[i].data()); |
| + } |
| + |
| + message += kUrlQueryParamSeparator; |
| + message += kOAuth2ScopeParamName + kKeyValSeparator; |
| + message += scopes_str; |
| + |
| + Request("https://accounts.google.com/o/oauth2/device/code", "POST", message, |
| + base::Bind(&GoogleAuthenticationServiceImpl::OnGetOAuth2DeviceCode, |
| + base::Unretained(this), callback)); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::AddAccount( |
| + const mojo::String& device_code, |
| + const AddAccountCallback& callback) { |
| + // Resets the poll count to "1" |
| + AddAccount(device_code, 1, callback); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::AddAccount( |
| + const mojo::String& device_code, |
| + const uint32_t num_poll_attempts, |
| + const AddAccountCallback& callback) { |
| + std::string message; |
| + message += |
| + kOAuth2ClientIdParamName + kKeyValSeparator + kMojoShellOAuth2ClientId; |
| + |
| + message += kUrlQueryParamSeparator; |
| + message += kOAuth2ClientSecretParamName + kKeyValSeparator + |
| + kMojoShellOAuth2ClientSecret; |
| + |
| + message += kUrlQueryParamSeparator; |
| + message += |
| + kOAuth2GrantTypeParamName + kKeyValSeparator + kOAuth2DeviceFlowGrantType; |
| + |
| + message += kUrlQueryParamSeparator; |
| + message += kOAuth2CodeParamName + kKeyValSeparator; |
| + message += device_code; |
| + |
| + Request("https://www.googleapis.com/oauth2/v3/token", "POST", message, |
| + base::Bind(&GoogleAuthenticationServiceImpl::OnAddAccount, |
| + base::Unretained(this), callback, device_code, |
| + num_poll_attempts)); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::OnGetOAuth2Token( |
| + const GetOAuth2TokenCallback& callback, |
| + const std::string& response, |
| + const std::string& error) { |
| + if (response.empty()) { |
| + callback.Run(nullptr, "Error from server:" + error); |
| + return; |
| + } |
| + |
| + scoped_ptr<base::DictionaryValue> dict = |
| + ParseOAuth2Response(response.c_str()); |
| + if (!dict || dict->HasKey("error")) { |
| + callback.Run(nullptr, "Error in parsing response:" + response); |
| + return; |
| + } |
| + |
| + std::string access_token; |
| + dict->GetString("access_token", &access_token); |
| + |
| + callback.Run(access_token, nullptr); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::OnGetOAuth2DeviceCode( |
| + const GetOAuth2DeviceCodeCallback& callback, |
| + const std::string& response, |
| + const std::string& error) { |
| + if (response.empty()) { |
| + callback.Run(nullptr, nullptr, nullptr, "Error from server:" + error); |
| + return; |
| + } |
| + |
| + scoped_ptr<base::DictionaryValue> dict = |
| + ParseOAuth2Response(response.c_str()); |
| + if (!dict || dict->HasKey("error")) { |
| + callback.Run(nullptr, nullptr, nullptr, |
| + "Error in parsing response:" + response); |
| + return; |
| + } |
| + |
| + std::string url; |
| + std::string device_code; |
| + std::string user_code; |
| + dict->GetString("verification_url", &url); |
| + dict->GetString("device_code", &device_code); |
| + dict->GetString("user_code", &user_code); |
| + |
| + callback.Run(url, device_code, user_code, nullptr); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::GetTokenInfo( |
| + const std::string& access_token) { |
| + std::string url("https://www.googleapis.com/oauth2/v1/tokeninfo"); |
| + url += "?access_token" + kKeyValSeparator; |
| + url += access_token; |
| + Request(url, "GET", "", |
| + base::Bind(&GoogleAuthenticationServiceImpl::OnGetTokenInfo, |
| + base::Unretained(this))); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::OnGetTokenInfo( |
| + const std::string& response, |
| + const std::string& error) { |
| + if (response.empty()) { |
| + return; |
| + } |
| + |
| + scoped_ptr<base::DictionaryValue> dict = |
| + ParseOAuth2Response(response.c_str()); |
| + if (!dict || dict->HasKey("error")) { |
| + return; |
| + } |
| + |
| + // This field is only present if the profile scope was present in the |
| + // request. The value of this field is an immutable identifier for the |
| + // logged-in user, and may be used when creating and managing user |
| + // sessions in your application. |
| + dict->GetString("user_id", &user_id_); |
| + dict->GetString("email", &email_); |
| + // The space-delimited set of scopes that the user consented to. |
| + dict->GetString("scope", &scope_); |
| + return; |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::GetUserInfo(const std::string& id_token) { |
| + std::string url("https://www.googleapis.com/oauth2/v1/tokeninfo"); |
| + url += "?id_token" + kKeyValSeparator; |
| + url += id_token; |
| + Request(url, "GET", "", |
| + base::Bind(&GoogleAuthenticationServiceImpl::OnGetUserInfo, |
| + base::Unretained(this))); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::OnGetUserInfo(const std::string& response, |
| + const std::string& error) { |
| + if (response.empty()) { |
| + return; |
| + } |
| + |
| + scoped_ptr<base::DictionaryValue> dict = |
| + ParseOAuth2Response(response.c_str()); |
| + if (!dict || dict->HasKey("error")) { |
| + return; |
| + } |
| + |
| + // This field is only present if the email scope was requested |
| + dict->GetString("email", &email_); |
| + |
| + return; |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::OnAddAccount( |
| + const AddAccountCallback& callback, |
| + const mojo::String& device_code, |
| + const uint32_t num_poll_attempts, |
| + const std::string& response, |
| + const std::string& error) { |
| + if (response.empty()) { |
| + callback.Run(nullptr, "Error from server:" + error); |
| + return; |
| + } |
| + |
| + if (!response.empty() && error.empty()) { |
| + scoped_ptr<base::Value> root(base::JSONReader::Read(response)); |
| + if (!root || !root->IsType(base::Value::TYPE_DICTIONARY)) { |
| + callback.Run(response, nullptr); |
| + return; |
| + } |
| + } |
| + |
| + // Parse response and fetch refresh, access and idtokens |
| + scoped_ptr<base::DictionaryValue> dict = |
| + ParseOAuth2Response(response.c_str()); |
| + std::string error_code; |
| + if (!dict) { |
| + callback.Run(nullptr, "Error in parsing response:" + response); |
| + return; |
| + } else if (dict->HasKey("error") && dict->GetString("error", &error_code)) { |
| + if (error_code != "authorization_pending") { |
| + callback.Run(nullptr, "Server error:" + response); |
| + return; |
| + } |
| + |
| + if (num_poll_attempts > 15) { |
| + callback.Run(nullptr, "Timed out after max number of polling attempts"); |
| + return; |
| + } |
| + |
| + // Rate limit by waiting 7 seconds before polling for a new grant |
| + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(7000)); |
| + AddAccount(device_code, num_poll_attempts + 1, |
| + base::Bind(&GoogleAuthenticationServiceImpl::OnAddAccount, |
| + base::Unretained(this), callback, device_code, |
| + num_poll_attempts + 1)); |
| + return; |
| + } |
| + |
| + // Poll success after detecting user grant. |
| + AuthData* auth_data = new AuthData(); |
| + std::string access_token; |
| + dict->GetString("access_token", &access_token); |
| + GetTokenInfo(access_token); // gets scope, email and user_id |
| + |
| + if (email_.empty()) { |
| + std::string id_token; |
| + dict->GetString("id_token", &id_token); |
| + GetUserInfo(id_token); // gets user's email |
| + } |
| + |
| + auth_data->username = email_.empty() ? user_id_ : email_; |
| + auth_data->scopes = scope_; |
| + auth_data->auth_provider = "Google"; |
| + auth_data->persistent_credential_type = "RT"; |
| + dict->GetString("refresh_token", &auth_data->persistent_credential); |
| + |
| + // TODO(ukode): Store access token in cache for the duration set in |
| + // response |
| + accounts_db_manager_->UpdateAccount( |
| + auth_data->username, authentication::GetAuthDataAsString(*auth_data)); |
| + |
| + callback.Run(auth_data->username, nullptr); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::Request( |
| + const std::string& url, |
| + const std::string& method, |
| + const std::string& message, |
| + const GetOAuth2TokenCallback& callback) { |
| + Request(url, method, message, callback, nullptr, 0); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::Request( |
| + const std::string& url, |
| + const std::string& method, |
| + const std::string& message, |
| + const GetOAuth2TokenCallback& callback, |
| + const mojo::String& device_code, |
| + const uint32_t num_poll_attempts) { |
| + mojo::URLRequestPtr request(mojo::URLRequest::New()); |
| + request->url = url; |
| + request->method = method; |
| + request->auto_follow_redirects = true; |
| + |
| + // Add headers |
| + auto content_type_header = mojo::HttpHeader::New(); |
| + content_type_header->name = "Content-Type"; |
| + content_type_header->value = "application/x-www-form-urlencoded"; |
| + request->headers.push_back(content_type_header.Pass()); |
| + |
| + if (!message.empty()) { |
| + request->body.push_back( |
| + mojo::common::WriteStringToConsumerHandle(message).Pass()); |
| + } |
| + |
| + mojo::URLLoaderPtr url_loader; |
| + network_service_->CreateURLLoader(GetProxy(&url_loader)); |
| + |
| + url_loader->Start( |
| + request.Pass(), |
| + base::Bind(&GoogleAuthenticationServiceImpl::HandleServerResponse, |
| + base::Unretained(this), callback, device_code, |
| + num_poll_attempts)); |
| + |
| + url_loader.WaitForIncomingResponse(); |
| +} |
| + |
| +void GoogleAuthenticationServiceImpl::HandleServerResponse( |
| + const GetOAuth2TokenCallback& callback, |
| + const mojo::String& device_code, |
| + const uint32_t num_poll_attempts, |
| + mojo::URLResponsePtr response) { |
| + if (response.is_null()) { |
| + LOG(WARNING) << "Something went horribly wrong...exiting!!"; |
| + callback.Run("", "Empty response"); |
| + return; |
| + } |
| + |
| + if (response->error) { |
| + LOG(ERROR) << "Got error (" << response->error->code |
| + << "), reason: " << response->error->description.get().c_str(); |
| + callback.Run("", response->error->description.get().c_str()); |
| + return; |
| + } |
| + |
| + std::string response_body; |
| + mojo::common::BlockingCopyToString(response->body.Pass(), &response_body); |
| + |
| + callback.Run(response_body, ""); |
| +} |
| + |
| +} // authentication namespace |