blob: fcbbeb737a1b47c63c7b36c3233b846096a45e05 [file] [log] [blame]
// 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 "chrome/browser/chromeos/settings/device_oauth2_token_service_delegate.h"
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/settings/token_encryptor.h"
#include "chrome/common/pref_names.h"
#include "chromeos/cryptohome/system_salt_getter.h"
#include "chromeos/settings/cros_settings_names.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "google_apis/gaia/oauth2_access_token_fetcher_impl.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace chromeos {
void DeviceOAuth2TokenServiceDelegate::OnServiceAccountIdentityChanged() {
if (!GetRobotAccountId().empty() && !refresh_token_.empty())
FireRefreshTokenAvailable(GetRobotAccountId());
}
DeviceOAuth2TokenServiceDelegate::DeviceOAuth2TokenServiceDelegate(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
PrefService* local_state)
: url_loader_factory_(url_loader_factory),
local_state_(local_state),
state_(STATE_LOADING),
max_refresh_token_validation_retries_(3),
validation_requested_(false),
validation_status_delegate_(nullptr),
service_account_identity_subscription_(
CrosSettings::Get()->AddSettingsObserver(
kServiceAccountIdentity,
base::Bind(&DeviceOAuth2TokenServiceDelegate::
OnServiceAccountIdentityChanged,
base::Unretained(this)))),
weak_ptr_factory_(this) {
// Pull in the system salt.
SystemSaltGetter::Get()->GetSystemSalt(
base::Bind(&DeviceOAuth2TokenServiceDelegate::DidGetSystemSalt,
weak_ptr_factory_.GetWeakPtr()));
}
DeviceOAuth2TokenServiceDelegate::~DeviceOAuth2TokenServiceDelegate() {
FlushTokenSaveCallbacks(false);
}
void DeviceOAuth2TokenServiceDelegate::SetAndSaveRefreshToken(
const std::string& refresh_token,
const StatusCallback& result_callback) {
ReportServiceError(GoogleServiceAuthError::REQUEST_CANCELED);
bool waiting_for_salt = state_ == STATE_LOADING;
refresh_token_ = refresh_token;
state_ = STATE_VALIDATION_PENDING;
// If the robot account ID is not available yet, do not announce the token. It
// will be done from OnServiceAccountIdentityChanged() once the robot account
// ID becomes available as well.
if (!GetRobotAccountId().empty())
FireRefreshTokenAvailable(GetRobotAccountId());
token_save_callbacks_.push_back(result_callback);
if (!waiting_for_salt) {
if (system_salt_.empty())
FlushTokenSaveCallbacks(false);
else
EncryptAndSaveToken();
}
}
bool DeviceOAuth2TokenServiceDelegate::RefreshTokenIsAvailable(
const std::string& account_id) const {
switch (state_) {
case STATE_NO_TOKEN:
case STATE_TOKEN_INVALID:
return false;
case STATE_LOADING:
case STATE_VALIDATION_PENDING:
case STATE_VALIDATION_STARTED:
case STATE_TOKEN_VALID:
return account_id == GetRobotAccountId();
}
NOTREACHED() << "Unhandled state " << state_;
return false;
}
std::string DeviceOAuth2TokenServiceDelegate::GetRobotAccountId() const {
std::string result;
CrosSettings::Get()->GetString(kServiceAccountIdentity, &result);
return result;
}
void DeviceOAuth2TokenServiceDelegate::OnRefreshTokenResponse(
const std::string& access_token,
int expires_in_seconds) {
gaia_oauth_client_->GetTokenInfo(access_token,
max_refresh_token_validation_retries_, this);
}
void DeviceOAuth2TokenServiceDelegate::OnGetTokenInfoResponse(
std::unique_ptr<base::DictionaryValue> token_info) {
std::string gaia_robot_id;
token_info->GetString("email", &gaia_robot_id);
gaia_oauth_client_.reset();
CheckRobotAccountId(gaia_robot_id);
}
void DeviceOAuth2TokenServiceDelegate::OnOAuthError() {
gaia_oauth_client_.reset();
state_ = STATE_TOKEN_INVALID;
ReportServiceError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
}
void DeviceOAuth2TokenServiceDelegate::OnNetworkError(int response_code) {
gaia_oauth_client_.reset();
// Go back to pending validation state. That'll allow a retry on subsequent
// token minting requests.
state_ = STATE_VALIDATION_PENDING;
ReportServiceError(GoogleServiceAuthError::CONNECTION_FAILED);
}
std::string DeviceOAuth2TokenServiceDelegate::GetRefreshToken(
const std::string& account_id) const {
switch (state_) {
case STATE_LOADING:
case STATE_NO_TOKEN:
case STATE_TOKEN_INVALID:
// This shouldn't happen: GetRefreshToken() is only called for actual
// token minting operations. In above states, requests are either queued
// or short-circuited to signal error immediately, so no actual token
// minting via OAuth2TokenService::FetchOAuth2Token should be triggered.
NOTREACHED();
return std::string();
case STATE_VALIDATION_PENDING:
case STATE_VALIDATION_STARTED:
case STATE_TOKEN_VALID:
return refresh_token_;
}
NOTREACHED() << "Unhandled state " << state_;
return std::string();
}
scoped_refptr<network::SharedURLLoaderFactory>
DeviceOAuth2TokenServiceDelegate::GetURLLoaderFactory() const {
return url_loader_factory_;
}
OAuth2AccessTokenFetcher*
DeviceOAuth2TokenServiceDelegate::CreateAccessTokenFetcher(
const std::string& account_id,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
OAuth2AccessTokenConsumer* consumer) {
std::string refresh_token = GetRefreshToken(account_id);
DCHECK(!refresh_token.empty());
return new OAuth2AccessTokenFetcherImpl(consumer, url_loader_factory,
refresh_token);
}
void DeviceOAuth2TokenServiceDelegate::DidGetSystemSalt(
const std::string& system_salt) {
system_salt_ = system_salt;
// Bail out if system salt is not available.
if (system_salt_.empty()) {
LOG(ERROR) << "Failed to get system salt.";
FlushTokenSaveCallbacks(false);
state_ = STATE_NO_TOKEN;
FireRefreshTokensLoaded();
return;
}
// If the token has been set meanwhile, write it to |local_state_|.
if (!refresh_token_.empty()) {
EncryptAndSaveToken();
FireRefreshTokensLoaded();
return;
}
// Otherwise, load the refresh token from |local_state_|.
std::string encrypted_refresh_token =
local_state_->GetString(prefs::kDeviceRobotAnyApiRefreshToken);
if (!encrypted_refresh_token.empty()) {
CryptohomeTokenEncryptor encryptor(system_salt_);
refresh_token_ = encryptor.DecryptWithSystemSalt(encrypted_refresh_token);
if (refresh_token_.empty()) {
LOG(ERROR) << "Failed to decrypt refresh token.";
state_ = STATE_NO_TOKEN;
FireRefreshTokensLoaded();
return;
}
}
state_ = STATE_VALIDATION_PENDING;
// If there are pending requests, start a validation.
if (validation_requested_)
StartValidation();
// Announce the token.
FireRefreshTokenAvailable(GetRobotAccountId());
FireRefreshTokensLoaded();
}
void DeviceOAuth2TokenServiceDelegate::CheckRobotAccountId(
const std::string& gaia_robot_id) {
// Make sure the value returned by GetRobotAccountId has been validated
// against current device settings.
switch (CrosSettings::Get()->PrepareTrustedValues(
base::Bind(&DeviceOAuth2TokenServiceDelegate::CheckRobotAccountId,
weak_ptr_factory_.GetWeakPtr(), gaia_robot_id))) {
case CrosSettingsProvider::TRUSTED:
// All good, compare account ids below.
break;
case CrosSettingsProvider::TEMPORARILY_UNTRUSTED:
// The callback passed to PrepareTrustedValues above will trigger a
// re-check eventually.
return;
case CrosSettingsProvider::PERMANENTLY_UNTRUSTED:
// There's no trusted account id, which is equivalent to no token present.
LOG(WARNING) << "Device settings permanently untrusted.";
state_ = STATE_NO_TOKEN;
ReportServiceError(GoogleServiceAuthError::USER_NOT_SIGNED_UP);
return;
}
std::string policy_robot_id = GetRobotAccountId();
if (policy_robot_id == gaia_robot_id) {
state_ = STATE_TOKEN_VALID;
ReportServiceError(GoogleServiceAuthError::NONE);
} else {
if (gaia_robot_id.empty()) {
LOG(WARNING) << "Device service account owner in policy is empty.";
} else {
LOG(WARNING) << "Device service account owner in policy does not match "
<< "refresh token owner \"" << gaia_robot_id << "\".";
}
state_ = STATE_TOKEN_INVALID;
ReportServiceError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);
}
}
void DeviceOAuth2TokenServiceDelegate::EncryptAndSaveToken() {
DCHECK_NE(state_, STATE_LOADING);
CryptohomeTokenEncryptor encryptor(system_salt_);
std::string encrypted_refresh_token =
encryptor.EncryptWithSystemSalt(refresh_token_);
bool result = true;
if (encrypted_refresh_token.empty()) {
LOG(ERROR) << "Failed to encrypt refresh token; save aborted.";
result = false;
} else {
local_state_->SetString(prefs::kDeviceRobotAnyApiRefreshToken,
encrypted_refresh_token);
}
FlushTokenSaveCallbacks(result);
}
void DeviceOAuth2TokenServiceDelegate::StartValidation() {
DCHECK_EQ(state_, STATE_VALIDATION_PENDING);
DCHECK(!gaia_oauth_client_);
state_ = STATE_VALIDATION_STARTED;
gaia_oauth_client_.reset(
new gaia::GaiaOAuthClient(g_browser_process->system_request_context()));
GaiaUrls* gaia_urls = GaiaUrls::GetInstance();
gaia::OAuthClientInfo client_info;
client_info.client_id = gaia_urls->oauth2_chrome_client_id();
client_info.client_secret = gaia_urls->oauth2_chrome_client_secret();
gaia_oauth_client_->RefreshToken(
client_info, refresh_token_,
std::vector<std::string>(1, GaiaConstants::kOAuthWrapBridgeUserInfoScope),
max_refresh_token_validation_retries_, this);
}
void DeviceOAuth2TokenServiceDelegate::FlushTokenSaveCallbacks(bool result) {
std::vector<StatusCallback> callbacks;
callbacks.swap(token_save_callbacks_);
for (std::vector<StatusCallback>::iterator callback(callbacks.begin());
callback != callbacks.end(); ++callback) {
if (!callback->is_null())
callback->Run(result);
}
}
void DeviceOAuth2TokenServiceDelegate::RequestValidation() {
validation_requested_ = true;
}
void DeviceOAuth2TokenServiceDelegate::SetValidationStatusDelegate(
ValidationStatusDelegate* delegate) {
validation_status_delegate_ = delegate;
}
void DeviceOAuth2TokenServiceDelegate::ReportServiceError(
GoogleServiceAuthError::State error) {
if (validation_status_delegate_) {
validation_status_delegate_->OnValidationCompleted(error);
}
}
} // namespace chromeos