blob: fa5360dff9154b94713a07729242e49b83e72e0b [file] [log] [blame]
// 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(
&param, 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