| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/device_identity/device_oauth2_token_service.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/device_identity/device_oauth2_token_store.h" |
| #include "components/policy/proto/device_management_backend.pb.h" |
| #include "components/prefs/pref_service.h" |
| #include "google_apis/gaia/gaia_access_token_fetcher.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_consumer.h" |
| #include "google_apis/gaia/oauth2_access_token_fetcher.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| |
| struct DeviceOAuth2TokenService::PendingRequest { |
| PendingRequest( |
| const base::WeakPtr<OAuth2AccessTokenManager::RequestImpl>& request, |
| const std::string& client_id, |
| const std::string& client_secret, |
| const OAuth2AccessTokenManager::ScopeSet& scopes) |
| : request(request), |
| client_id(client_id), |
| client_secret(client_secret), |
| scopes(scopes) {} |
| |
| const base::WeakPtr<OAuth2AccessTokenManager::RequestImpl> request; |
| const std::string client_id; |
| const std::string client_secret; |
| const OAuth2AccessTokenManager::ScopeSet scopes; |
| }; |
| |
| DeviceOAuth2TokenService::DeviceOAuth2TokenService( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| std::unique_ptr<DeviceOAuth2TokenStore> store) |
| : url_loader_factory_(url_loader_factory), |
| state_(STATE_LOADING), |
| max_refresh_token_validation_retries_(3), |
| validation_requested_(false), |
| store_(std::move(store)) { |
| token_manager_ = std::make_unique<OAuth2AccessTokenManager>( |
| this /* OAuth2AccessTokenManager::Delegate* */); |
| store_->SetObserver(this); |
| store_->Init(base::BindOnce(&DeviceOAuth2TokenService::OnInitComplete, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| DeviceOAuth2TokenService::~DeviceOAuth2TokenService() { |
| FlushPendingRequests(false, GoogleServiceAuthError::REQUEST_CANCELED); |
| } |
| |
| void DeviceOAuth2TokenService::SetAndSaveRefreshToken( |
| const std::string& refresh_token, |
| StatusCallback result_callback) { |
| ReportServiceError(GoogleServiceAuthError::REQUEST_CANCELED); |
| |
| state_ = STATE_VALIDATION_PENDING; |
| store_->SetAndSaveRefreshToken(refresh_token, std::move(result_callback)); |
| } |
| |
| CoreAccountId DeviceOAuth2TokenService::GetRobotAccountId() const { |
| if (!robot_account_id_for_testing_.empty()) { |
| return robot_account_id_for_testing_; |
| } |
| |
| return store_->GetAccountId(); |
| } |
| |
| void DeviceOAuth2TokenService::set_robot_account_id_for_testing( |
| const CoreAccountId& account_id) { |
| robot_account_id_for_testing_ = account_id; |
| } |
| |
| void DeviceOAuth2TokenService::SetRefreshTokenAvailableCallback( |
| RefreshTokenAvailableCallback callback) { |
| on_refresh_token_available_callback_ = std::move(callback); |
| } |
| |
| std::unique_ptr<OAuth2AccessTokenManager::Request> |
| DeviceOAuth2TokenService::StartAccessTokenRequest( |
| const OAuth2AccessTokenManager::ScopeSet& scopes, |
| OAuth2AccessTokenManager::Consumer* consumer) { |
| // Note: It is fine to pass an empty account id to |token_manager_| as this |
| // will just return a request that will always fail. |
| return token_manager_->StartRequest(GetRobotAccountId(), scopes, consumer); |
| } |
| |
| void DeviceOAuth2TokenService::InvalidateAccessToken( |
| const OAuth2AccessTokenManager::ScopeSet& scopes, |
| const std::string& access_token) { |
| if (GetRobotAccountId().empty()) |
| return; |
| |
| token_manager_->InvalidateAccessToken(GetRobotAccountId(), scopes, |
| access_token); |
| } |
| |
| bool DeviceOAuth2TokenService::RefreshTokenIsAvailable() 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 !GetRobotAccountId().empty(); |
| } |
| |
| NOTREACHED() << "Unhandled state " << state_; |
| return false; |
| } |
| |
| OAuth2AccessTokenManager* DeviceOAuth2TokenService::GetAccessTokenManager() { |
| return token_manager_.get(); |
| } |
| |
| #if !BUILDFLAG(IS_CHROMEOS_ASH) |
| void DeviceOAuth2TokenService::SetServiceAccountEmail( |
| const std::string& account_email) { |
| store_->SetAccountEmail(account_email); |
| } |
| #endif |
| |
| void DeviceOAuth2TokenService::OnRefreshTokenResponse( |
| const std::string& access_token, |
| int expires_in_seconds) { |
| gaia_oauth_client_->GetTokenInfo(access_token, |
| max_refresh_token_validation_retries_, this); |
| } |
| |
| void DeviceOAuth2TokenService::OnGetTokenInfoResponse( |
| const base::Value::Dict& token_info) { |
| // For robot accounts email id is the account id. |
| const std::string* robot_email = token_info.FindString("email"); |
| gaia_oauth_client_.reset(); |
| |
| store_->PrepareTrustedAccountId(base::BindRepeating( |
| &DeviceOAuth2TokenService::OnPrepareTrustedAccountIdFinished, |
| weak_ptr_factory_.GetWeakPtr(), |
| CoreAccountId::FromRobotEmail(robot_email ? *robot_email : ""))); |
| } |
| |
| void DeviceOAuth2TokenService::OnOAuthError() { |
| gaia_oauth_client_.reset(); |
| state_ = STATE_TOKEN_INVALID; |
| ReportServiceError(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); |
| } |
| |
| void DeviceOAuth2TokenService::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); |
| } |
| |
| void DeviceOAuth2TokenService::OnInitComplete(bool init_result, |
| bool validation_required) { |
| if (!init_result) { |
| state_ = STATE_NO_TOKEN; |
| return; |
| } |
| |
| // If the refresh token was set while waiting for the init, it was already |
| // validated and |validation_required| here will be false. In that case, no |
| // point validating it again; bail out. |
| if (!validation_required) |
| return; |
| |
| state_ = STATE_VALIDATION_PENDING; |
| |
| // If there are pending requests, start a validation. |
| if (validation_requested_) |
| StartValidation(); |
| |
| // Announce the token. |
| if (!GetRobotAccountId().empty()) |
| FireRefreshTokenAvailable(); |
| } |
| |
| void DeviceOAuth2TokenService::OnPrepareTrustedAccountIdFinished( |
| const CoreAccountId& gaia_robot_id, |
| bool check_passed) { |
| if (!check_passed) { |
| state_ = STATE_NO_TOKEN; |
| ReportServiceError(GoogleServiceAuthError::USER_NOT_SIGNED_UP); |
| return; |
| } |
| |
| CoreAccountId 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 DeviceOAuth2TokenService::OnRefreshTokenAvailable() { |
| FireRefreshTokenAvailable(); |
| } |
| |
| std::unique_ptr<OAuth2AccessTokenFetcher> |
| DeviceOAuth2TokenService::CreateAccessTokenFetcher( |
| const CoreAccountId& account_id, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| OAuth2AccessTokenConsumer* consumer) { |
| std::string refresh_token = GetRefreshToken(); |
| DCHECK(!refresh_token.empty()); |
| return GaiaAccessTokenFetcher:: |
| CreateExchangeRefreshTokenForAccessTokenInstance( |
| consumer, url_loader_factory, refresh_token); |
| } |
| |
| bool DeviceOAuth2TokenService::HasRefreshToken( |
| const CoreAccountId& account_id) const { |
| if (account_id.empty()) |
| return false; |
| |
| if (GetRobotAccountId() != account_id) |
| return false; |
| |
| return RefreshTokenIsAvailable(); |
| } |
| |
| scoped_refptr<network::SharedURLLoaderFactory> |
| DeviceOAuth2TokenService::GetURLLoaderFactory() const { |
| return url_loader_factory_; |
| } |
| |
| void DeviceOAuth2TokenService::FireRefreshTokenAvailable() { |
| if (!on_refresh_token_available_callback_) |
| return; |
| |
| DCHECK(!GetRobotAccountId().empty()); |
| on_refresh_token_available_callback_.Run(); |
| } |
| |
| bool DeviceOAuth2TokenService::HandleAccessTokenFetch( |
| OAuth2AccessTokenManager::RequestImpl* request, |
| const CoreAccountId& account_id, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| const std::string& client_id, |
| const std::string& client_secret, |
| const OAuth2AccessTokenManager::ScopeSet& scopes) { |
| if (!HasValidationResult()) { |
| // Add a pending request that will be satisfied once validation completes. |
| // This must happen before the call to |StartValidation()| because the |
| // latter can be synchronous on some platforms. |
| pending_requests_.push_back(new PendingRequest( |
| request->AsWeakPtr(), client_id, client_secret, scopes)); |
| RequestValidation(); |
| } |
| |
| switch (state_) { |
| case STATE_VALIDATION_PENDING: |
| // If this is the first request for a token, start validation. |
| StartValidation(); |
| return true; |
| case STATE_LOADING: |
| case STATE_VALIDATION_STARTED: |
| return true; |
| case STATE_NO_TOKEN: |
| FailRequest(request, GoogleServiceAuthError::USER_NOT_SIGNED_UP); |
| return true; |
| case STATE_TOKEN_INVALID: |
| FailRequest(request, GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); |
| return true; |
| case STATE_TOKEN_VALID: |
| // Let OAuth2AccessTokenManager handle the request. |
| return false; |
| } |
| |
| NOTREACHED() << "Unexpected state " << state_; |
| return false; |
| } |
| |
| void DeviceOAuth2TokenService::FlushPendingRequests( |
| bool token_is_valid, |
| GoogleServiceAuthError::State error) { |
| std::vector<PendingRequest*> requests; |
| requests.swap(pending_requests_); |
| for (std::vector<PendingRequest*>::iterator request(requests.begin()); |
| request != requests.end(); ++request) { |
| std::unique_ptr<PendingRequest> scoped_request(*request); |
| if (!scoped_request->request) |
| continue; |
| |
| if (token_is_valid) { |
| token_manager_->FetchOAuth2Token( |
| scoped_request->request.get(), |
| scoped_request->request->GetAccountId(), GetURLLoaderFactory(), |
| scoped_request->client_id, scoped_request->client_secret, |
| scoped_request->request->GetConsumerId(), scoped_request->scopes); |
| } else { |
| FailRequest(scoped_request->request.get(), error); |
| } |
| } |
| } |
| |
| void DeviceOAuth2TokenService::FailRequest( |
| OAuth2AccessTokenManager::RequestImpl* request, |
| GoogleServiceAuthError::State error) { |
| GoogleServiceAuthError auth_error = |
| (error == GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS) |
| ? GoogleServiceAuthError::FromInvalidGaiaCredentialsReason( |
| GoogleServiceAuthError::InvalidGaiaCredentialsReason:: |
| CREDENTIALS_REJECTED_BY_SERVER) |
| : GoogleServiceAuthError(error); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&OAuth2AccessTokenManager::RequestImpl::InformConsumer, |
| request->AsWeakPtr(), auth_error, |
| OAuth2AccessTokenConsumer::TokenResponse())); |
| } |
| |
| std::string DeviceOAuth2TokenService::GetRefreshToken() 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 OAuth2AccessTokenManager::FetchOAuth2Token should be |
| // triggered. |
| NOTREACHED(); |
| return std::string(); |
| case STATE_VALIDATION_PENDING: |
| case STATE_VALIDATION_STARTED: |
| case STATE_TOKEN_VALID: |
| return store_->GetRefreshToken(); |
| } |
| |
| NOTREACHED() << "Unhandled state " << state_; |
| return std::string(); |
| } |
| |
| void DeviceOAuth2TokenService::StartValidation() { |
| DCHECK_EQ(state_, STATE_VALIDATION_PENDING); |
| DCHECK(!gaia_oauth_client_); |
| |
| state_ = STATE_VALIDATION_STARTED; |
| |
| gaia_oauth_client_ = |
| std::make_unique<gaia::GaiaOAuthClient>(url_loader_factory_); |
| |
| 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, store_->GetRefreshToken(), |
| std::vector<std::string>(1, GaiaConstants::kGoogleUserInfoEmail), |
| max_refresh_token_validation_retries_, this); |
| } |
| |
| void DeviceOAuth2TokenService::RequestValidation() { |
| validation_requested_ = true; |
| } |
| |
| void DeviceOAuth2TokenService::ReportServiceError( |
| GoogleServiceAuthError::State error) { |
| if (error == GoogleServiceAuthError::NONE) |
| FlushPendingRequests(true, GoogleServiceAuthError::NONE); |
| else |
| FlushPendingRequests(false, error); |
| } |
| |
| bool DeviceOAuth2TokenService::HasValidationResult() const { |
| return state_ == STATE_NO_TOKEN || state_ == STATE_TOKEN_INVALID || |
| state_ == STATE_TOKEN_VALID; |
| } |