| 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..fa5360dff9154b94713a07729242e49b83e72e0b
|
| --- /dev/null
|
| +++ b/services/authentication/google_authentication_impl.cc
|
| @@ -0,0 +1,457 @@
|
| +// Copyright 2016 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/message_loop/message_loop.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/strings/stringprintf.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/credentials_impl_db.mojom.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
|
| +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";
|
| +
|
| +// TODO(ukode) : Verify the char list
|
| +char kEscapableUrlParamChars[] = ".$[]/";
|
| +
|
| +std::string EncodeParam(std::string param) {
|
| + for (size_t i = 0; i < strlen(kEscapableUrlParamChars); ++i) {
|
| + base::ReplaceSubstringsAfterOffset(
|
| + ¶m, 0, std::string(1, kEscapableUrlParamChars[i]),
|
| + base::StringPrintf("%%%x", kEscapableUrlParamChars[i]));
|
| + }
|
| + return param;
|
| +}
|
| +
|
| +mojo::String BuildUrlQuery(mojo::Map<mojo::String, mojo::String> params) {
|
| + std::string message;
|
| + for (auto it = params.begin(); it != params.end(); ++it) {
|
| + message += EncodeParam(it.GetKey()) + "=" + EncodeParam(it.GetValue());
|
| + message += "&";
|
| + }
|
| +
|
| + if (!message.empty()) {
|
| + message = message.substr(0, message.size() - 1); // Trims extra "&".
|
| + }
|
| + return message;
|
| +}
|
| +
|
| +static 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;
|
| + }
|
| +
|
| + return static_cast<base::DictionaryValue*>(root.release());
|
| +}
|
| +
|
| +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) {
|
| + accounts_db_manager_ = new AccountsDbManager(directory.Pass());
|
| +}
|
| +
|
| +GoogleAuthenticationServiceImpl::~GoogleAuthenticationServiceImpl() {
|
| + delete accounts_db_manager_;
|
| +}
|
| +
|
| +void GoogleAuthenticationServiceImpl::GetOAuth2Token(
|
| + const mojo::String& username,
|
| + mojo::Array<mojo::String> scopes,
|
| + const GetOAuth2TokenCallback& callback) {
|
| + if (!accounts_db_manager_->isValid()) {
|
| + callback.Run(nullptr, "Accounts db validation failed.");
|
| + return;
|
| + }
|
| +
|
| + authentication::CredentialsPtr creds =
|
| + accounts_db_manager_->GetCredentials(username);
|
| +
|
| + if (!creds->token) {
|
| + 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.
|
| + mojo::Map<mojo::String, mojo::String> params;
|
| + params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId;
|
| + params[kOAuth2ClientSecretParamName] = kMojoShellOAuth2ClientSecret;
|
| + params[kOAuth2GrantTypeParamName] = kOAuth2RefreshTokenGrantType;
|
| + params[kOAuth2RefreshTokenParamName] = creds->token;
|
| +
|
| + Request("https://www.googleapis.com/oauth2/v3/token", "POST",
|
| + BuildUrlQuery(params.Pass()),
|
| + base::Bind(&GoogleAuthenticationServiceImpl::OnGetOAuth2Token,
|
| + base::Unretained(this), callback));
|
| +}
|
| +
|
| +void GoogleAuthenticationServiceImpl::SelectAccount(
|
| + bool returnLastSelected,
|
| + const SelectAccountCallback& callback) {
|
| + if (!accounts_db_manager_->isValid()) {
|
| + callback.Run(nullptr, "Accounts db validation failed.");
|
| + return;
|
| + }
|
| +
|
| + 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::Array<mojo::String> users = accounts_db_manager_->GetAllUsers();
|
| + if (!users.size()) {
|
| + callback.Run(nullptr, "No user accounts found.");
|
| + return;
|
| + }
|
| +
|
| + username = users[0];
|
| + 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 scopes_str("email");
|
| + for (size_t i = 0; i < scopes.size(); i++) {
|
| + scopes_str += " ";
|
| + scopes_str += std::string(scopes[i].data());
|
| + }
|
| +
|
| + mojo::Map<mojo::String, mojo::String> params;
|
| + params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId;
|
| + params[kOAuth2ScopeParamName] = scopes_str;
|
| +
|
| + Request("https://accounts.google.com/o/oauth2/device/code", "POST",
|
| + BuildUrlQuery(params.Pass()),
|
| + 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"
|
| + AddAccountInternal(device_code, 1, callback);
|
| +}
|
| +
|
| +void GoogleAuthenticationServiceImpl::AddAccountInternal(
|
| + const mojo::String& device_code,
|
| + const uint32_t num_poll_attempts,
|
| + const AddAccountCallback& callback) {
|
| + mojo::Map<mojo::String, mojo::String> params;
|
| + params[kOAuth2ClientIdParamName] = kMojoShellOAuth2ClientId;
|
| + params[kOAuth2ClientSecretParamName] = kMojoShellOAuth2ClientSecret;
|
| + params[kOAuth2GrantTypeParamName] = kOAuth2DeviceFlowGrantType;
|
| + params[kOAuth2CodeParamName] = device_code;
|
| +
|
| + Request("https://www.googleapis.com/oauth2/v3/token", "POST",
|
| + BuildUrlQuery(params.Pass()),
|
| + 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.get() || 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.get() || 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=" + EncodeParam(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.get() || 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=" + EncodeParam(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.get() || dict->HasKey("error")) {
|
| + return;
|
| + }
|
| +
|
| + // This field is only present if the email scope was requested
|
| + dict->GetString("email", &email_);
|
| +}
|
| +
|
| +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.get()) {
|
| + 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::MessageLoop::current()->PostDelayedTask(
|
| + FROM_HERE,
|
| + base::Bind(&GoogleAuthenticationServiceImpl::AddAccountInternal,
|
| + base::Unretained(this), device_code, num_poll_attempts + 1,
|
| + callback),
|
| + base::TimeDelta::FromMilliseconds(7000));
|
| + return;
|
| + }
|
| +
|
| + // Poll success, after detecting user grant.
|
| + 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
|
| + }
|
| +
|
| + // TODO(ukode): Store access token in cache for the duration set in
|
| + // response
|
| + if (!accounts_db_manager_->isValid()) {
|
| + callback.Run(nullptr, "Accounts db validation failed.");
|
| + return;
|
| + }
|
| +
|
| + std::string refresh_token;
|
| + dict->GetString("refresh_token", &refresh_token);
|
| + authentication::CredentialsPtr creds = authentication::Credentials::New();
|
| + creds->token = refresh_token;
|
| + creds->scopes = scope_;
|
| + creds->auth_provider = AuthProvider::GOOGLE;
|
| + creds->credential_type = CredentialType::DOWNSCOPED_OAUTH_REFRESH_TOKEN;
|
| + std::string username = email_.empty() ? user_id_ : email_;
|
| + accounts_db_manager_->UpdateCredentials(username, creds.Pass());
|
| +
|
| + callback.Run(username, nullptr);
|
| +}
|
| +
|
| +void GoogleAuthenticationServiceImpl::Request(
|
| + const std::string& url,
|
| + const std::string& method,
|
| + const std::string& message,
|
| + const mojo::Callback<void(std::string, std::string)>& callback) {
|
| + Request(url, method, message, callback, nullptr, 0);
|
| +}
|
| +
|
| +void GoogleAuthenticationServiceImpl::Request(
|
| + const std::string& url,
|
| + const std::string& method,
|
| + const std::string& message,
|
| + const mojo::Callback<void(std::string, std::string)>& 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 mojo::Callback<void(std::string, std::string)>& 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
|
|
|