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