| // Copyright 2020 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/ash/login/saml/password_sync_token_fetcher.h" |
| |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "components/account_id/account_id.h" |
| #include "components/signin/public/identity_manager/access_token_fetcher.h" |
| #include "components/signin/public/identity_manager/access_token_info.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h" |
| #include "components/signin/public/identity_manager/scope_set.h" |
| #include "components/user_manager/known_user.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/common/url_constants.h" |
| #include "google_apis/credentials_mode.h" |
| #include "google_apis/gaia/gaia_auth_fetcher.h" |
| #include "google_apis/gaia/gaia_constants.h" |
| #include "google_apis/gaia/google_service_auth_error.h" |
| #include "google_apis/google_api_keys.h" |
| #include "net/base/load_flags.h" |
| #include "net/http/http_status_code.h" |
| #include "net/url_request/url_request_context_getter.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| |
| namespace ash { |
| namespace { |
| |
| // These values should not be renumbered and numeric values should never |
| // be reused. This must be kept in sync with SamlInSessionPasswordSyncEvent |
| // in tools/metrics/histogram/enums.xml |
| enum class InSessionPasswordSyncEvent { |
| kStartPollingInSession = 0, |
| kStartPollingOnLogin = 1, |
| kTokenValidationSuccess = 2, |
| kTokenValidationFailure = 3, |
| kErrorMissingAccessToken = 4, |
| kErrorWrongResponseCode = 5, |
| kErrorInSerializedResponse = 6, |
| kErrorNoTokenInCreateResponse = 7, |
| kErrorNoTokenInGetResponse = 8, |
| kMaxValue = kErrorNoTokenInGetResponse, |
| }; |
| |
| constexpr int kGetAuthCodeNetworkRetry = 1; |
| constexpr int kMaxResponseSize = 5 * 1024; |
| const char kAccessTokenFetchId[] = "sync_token_fetcher"; |
| |
| const char kErrorKey[] = "error"; |
| const char kErrorDescription[] = "message"; |
| const char kToken[] = "name"; |
| const char kTokenEntry[] = "token"; |
| const char kTokenStatusKey[] = "tokenStatus"; |
| const char kTokenStatusValid[] = "VALID"; |
| const char kAuthorizationHeaderFormat[] = "Bearer %s"; |
| const char kContentTypeJSON[] = "application/json"; |
| const char kTokenTypeKey[] = "token_type"; |
| const char kTokenTypeValue[] = "SAML_PASSWORD"; |
| const char kAcceptValue[] = |
| "Accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; |
| |
| const char kPasswordSyncTokenBaseEndPoint[] = |
| "https://chromedevicetoken.googleapis.com/v1/tokens"; |
| |
| const char kPasswordSyncTokenCreateEndPoint[] = ""; |
| |
| const char kPasswordSyncTokenGetEndPoint[] = "?token_type=SAML_PASSWORD"; |
| |
| const char kPasswordSyncTokenVerifyEndPoint[] = |
| "/%s:verify?token_type=SAML_PASSWORD&key=%s"; |
| |
| std::string GetBaseEndPoint() { |
| return kPasswordSyncTokenBaseEndPoint; |
| } |
| |
| GURL GetSyncTokenCreateUrl() { |
| return GURL(GetBaseEndPoint() + |
| std::string(kPasswordSyncTokenCreateEndPoint)); |
| } |
| |
| GURL GetSyncTokenGetUrl() { |
| return GURL(GetBaseEndPoint() + std::string(kPasswordSyncTokenGetEndPoint)); |
| } |
| |
| GURL GetSyncTokenVerifyUrl(const std::string& sync_token, |
| const std::string& escaped_api_key) { |
| return GURL(GetBaseEndPoint() + |
| base::StringPrintf(kPasswordSyncTokenVerifyEndPoint, |
| sync_token.c_str(), escaped_api_key.c_str())); |
| } |
| |
| void RecordEvent(InSessionPasswordSyncEvent event) { |
| base::UmaHistogramEnumeration("ChromeOS.SAML.InSessionPasswordSyncEvent", |
| event); |
| } |
| |
| } // namespace |
| |
| void RecordStartOfSyncTokenPollingUMA(bool in_session) { |
| RecordEvent(in_session ? InSessionPasswordSyncEvent::kStartPollingInSession |
| : InSessionPasswordSyncEvent::kStartPollingOnLogin); |
| } |
| |
| PasswordSyncTokenFetcher::Consumer::Consumer() = default; |
| |
| PasswordSyncTokenFetcher::Consumer::~Consumer() = default; |
| |
| PasswordSyncTokenFetcher::PasswordSyncTokenFetcher( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| Profile* profile, |
| Consumer* consumer) |
| : url_loader_factory_(std::move(url_loader_factory)), |
| profile_(profile), |
| consumer_(consumer), |
| request_type_(RequestType::kNone) { |
| DCHECK(consumer_); |
| } |
| |
| PasswordSyncTokenFetcher::~PasswordSyncTokenFetcher() = default; |
| |
| void PasswordSyncTokenFetcher::StartTokenCreate() { |
| DCHECK_EQ(request_type_, RequestType::kNone); |
| request_type_ = RequestType::kCreateToken; |
| StartAccessTokenFetch(); |
| } |
| |
| void PasswordSyncTokenFetcher::StartTokenGet() { |
| DCHECK_EQ(request_type_, RequestType::kNone); |
| request_type_ = RequestType::kGetToken; |
| StartAccessTokenFetch(); |
| } |
| |
| void PasswordSyncTokenFetcher::StartTokenVerify(const std::string& sync_token) { |
| DCHECK_EQ(request_type_, RequestType::kNone); |
| request_type_ = RequestType::kVerifyToken; |
| sync_token_ = sync_token; |
| FetchSyncToken(/*access_token=*/std::string()); |
| } |
| |
| void PasswordSyncTokenFetcher::StartAccessTokenFetch() { |
| DCHECK(profile_); |
| signin::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForProfile(profile_); |
| DCHECK(identity_manager); |
| |
| // Now we can request the token, knowing that it will be immediately requested |
| // if the refresh token is available, or that it will be requested once the |
| // refresh token is available for the primary account. |
| signin::ScopeSet scopes; |
| scopes.insert(GaiaConstants::kGoogleUserInfoEmail); |
| scopes.insert(GaiaConstants::kDeviceManagementServiceOAuth); |
| |
| access_token_fetcher_ = |
| std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( |
| kAccessTokenFetchId, identity_manager, scopes, |
| base::BindOnce(&PasswordSyncTokenFetcher::OnAccessTokenFetchComplete, |
| weak_ptr_factory_.GetWeakPtr()), |
| signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable, |
| signin::ConsentLevel::kSignin); |
| } |
| |
| void PasswordSyncTokenFetcher::OnAccessTokenFetchComplete( |
| GoogleServiceAuthError error, |
| signin::AccessTokenInfo token_info) { |
| access_token_fetcher_.reset(); |
| |
| if (error.state() != GoogleServiceAuthError::NONE) { |
| LOG(ERROR) |
| << "Could not get access token to authorize sync token operation: " |
| << error.ToString(); |
| RecordEvent(InSessionPasswordSyncEvent::kErrorMissingAccessToken); |
| consumer_->OnApiCallFailed(ErrorType::kMissingAccessToken); |
| return; |
| } |
| FetchSyncToken(token_info.token); |
| } |
| |
| void PasswordSyncTokenFetcher::FetchSyncToken(const std::string& access_token) { |
| base::Value request_data(base::Value::Type::DICT); |
| request_data.SetStringKey(kTokenTypeKey, kTokenTypeValue); |
| std::string request_string; |
| if (!base::JSONWriter::Write(request_data, &request_string)) { |
| LOG(ERROR) << "Not able to serialize token request body."; |
| consumer_->OnApiCallFailed(ErrorType::kRequestBodyNotSerialized); |
| return; |
| } |
| const net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("password_sync_token_fetcher", R"( |
| semantics { |
| sender: "Chrome OS sync token fetcher" |
| description: |
| "Call password sync token API used to synchronize SAML credentials" |
| "between multiple user devices." |
| trigger: |
| "When SAML password is changed in session or device initiates check " |
| "of the local version of password sync token. When the token is " |
| "invalid device requests online re-authentication of the user in " |
| "order to sync user's password and update the token." |
| data: "Access token and token_type." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting : "Only Admins can enable/disable this feature from the admin" |
| "dashboard." |
| chrome_policy { |
| SamlInSessionPasswordChangeEnabled { |
| SamlInSessionPasswordChangeEnabled : false |
| } |
| } |
| })"); |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| switch (request_type_) { |
| case RequestType::kCreateToken: |
| resource_request->url = GetSyncTokenCreateUrl(); |
| break; |
| case RequestType::kGetToken: |
| resource_request->url = GetSyncTokenGetUrl(); |
| break; |
| case RequestType::kVerifyToken: |
| resource_request->url = GetSyncTokenVerifyUrl( |
| sync_token_, base::EscapeQueryParamValue(google_apis::GetAPIKey(), |
| /*use_plus=*/true)); |
| break; |
| case RequestType::kNone: |
| // Error: request type needs to be already set. |
| NOTREACHED(); |
| } |
| resource_request->load_flags = |
| net::LOAD_DISABLE_CACHE | net::LOAD_BYPASS_CACHE; |
| resource_request->credentials_mode = |
| google_apis::GetOmitCredentialsModeForGaiaRequests(); |
| if (request_type_ == RequestType::kCreateToken) { |
| resource_request->method = net::HttpRequestHeaders::kPostMethod; |
| } else { |
| resource_request->method = net::HttpRequestHeaders::kGetMethod; |
| } |
| if (request_type_ != RequestType::kVerifyToken) { |
| resource_request->headers.SetHeader( |
| net::HttpRequestHeaders::kAuthorization, |
| base::StringPrintf(kAuthorizationHeaderFormat, access_token.c_str())); |
| } |
| resource_request->headers.SetHeader(net::HttpRequestHeaders::kContentType, |
| kContentTypeJSON); |
| resource_request->headers.SetHeader(net::HttpRequestHeaders::kAccept, |
| kAcceptValue); |
| DCHECK(!simple_url_loader_); |
| |
| simple_url_loader_ = network::SimpleURLLoader::Create( |
| std::move(resource_request), traffic_annotation); |
| if (request_type_ == RequestType::kCreateToken) { |
| simple_url_loader_->AttachStringForUpload(request_string, kContentTypeJSON); |
| } |
| simple_url_loader_->SetRetryOptions( |
| kGetAuthCodeNetworkRetry, |
| network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE); |
| simple_url_loader_->SetAllowHttpErrorResults(true); |
| simple_url_loader_->DownloadToString( |
| url_loader_factory_.get(), |
| base::BindOnce(&PasswordSyncTokenFetcher::OnSimpleLoaderComplete, |
| weak_ptr_factory_.GetWeakPtr()), |
| kMaxResponseSize); |
| } |
| |
| void PasswordSyncTokenFetcher::OnSimpleLoaderComplete( |
| std::unique_ptr<std::string> response_body) { |
| int response_code = -1; |
| if (simple_url_loader_->ResponseInfo() && |
| simple_url_loader_->ResponseInfo()->headers) { |
| response_code = |
| simple_url_loader_->ResponseInfo()->headers->response_code(); |
| } |
| std::string json_string; |
| if (response_body) |
| json_string = std::move(*response_body); |
| simple_url_loader_.reset(); |
| |
| JSONStringValueDeserializer deserializer(json_string); |
| std::string error_msg; |
| std::unique_ptr<base::Value> json_value = |
| deserializer.Deserialize(/*error_code=*/nullptr, &error_msg); |
| |
| if (!response_body || (response_code != net::HTTP_OK)) { |
| const auto* error_json = json_value && json_value->is_dict() |
| ? json_value->GetDict().FindDict(kErrorKey) |
| : nullptr; |
| const std::string* error = |
| error_json ? error_json->FindString(kErrorDescription) : nullptr; |
| |
| LOG(WARNING) << "Server returned wrong response code: " << response_code |
| << ": " << (error ? *error : "Unknown") << "."; |
| RecordEvent(InSessionPasswordSyncEvent::kErrorWrongResponseCode); |
| consumer_->OnApiCallFailed(ErrorType::kServerError); |
| return; |
| } |
| |
| if (!json_value) { |
| LOG(WARNING) << "Unable to deserialize json data."; |
| RecordEvent(InSessionPasswordSyncEvent::kErrorInSerializedResponse); |
| consumer_->OnApiCallFailed(ErrorType::kInvalidJson); |
| return; |
| } |
| |
| if (!json_value->is_dict()) { |
| LOG(WARNING) << "Response is not a JSON dictionary."; |
| RecordEvent(InSessionPasswordSyncEvent::kErrorInSerializedResponse); |
| consumer_->OnApiCallFailed(ErrorType::kNotJsonDict); |
| return; |
| } |
| |
| ProcessValidTokenResponse(std::move(json_value->GetDict())); |
| } |
| |
| void PasswordSyncTokenFetcher::ProcessValidTokenResponse( |
| base::Value::Dict json_response) { |
| switch (request_type_) { |
| case RequestType::kCreateToken: { |
| const std::string* sync_token = json_response.FindString(kToken); |
| if (!sync_token || sync_token->empty()) { |
| LOG(WARNING) << "Response does not contain sync token."; |
| RecordEvent(InSessionPasswordSyncEvent::kErrorNoTokenInCreateResponse); |
| consumer_->OnApiCallFailed(ErrorType::kCreateNoToken); |
| return; |
| } |
| consumer_->OnTokenCreated(*sync_token); |
| break; |
| } |
| case RequestType::kGetToken: { |
| std::string sync_token; |
| const auto* token_list_entry = json_response.FindList(kTokenEntry); |
| if (!token_list_entry) { |
| LOG(WARNING) << "Response does not contain list of sync tokens."; |
| RecordEvent(InSessionPasswordSyncEvent::kErrorNoTokenInGetResponse); |
| consumer_->OnApiCallFailed(ErrorType::kGetNoList); |
| return; |
| } |
| const base::Value::List& list_of_tokens = *token_list_entry; |
| if (list_of_tokens.size() > 0) { |
| const std::string* sync_token_string = |
| list_of_tokens[0].GetDict().FindString(kToken); |
| if (!sync_token_string || sync_token_string->empty()) { |
| LOG(WARNING) << "Response does not contain sync token."; |
| RecordEvent(InSessionPasswordSyncEvent::kErrorNoTokenInGetResponse); |
| consumer_->OnApiCallFailed(ErrorType::kGetNoToken); |
| return; |
| } |
| sync_token = *sync_token_string; |
| } |
| // list_of_tokens.size() == 0 is still a valid case here - it means we |
| // have not created any token for this user yet. |
| consumer_->OnTokenFetched(sync_token); |
| break; |
| } |
| case RequestType::kVerifyToken: { |
| const std::string* sync_token_status = |
| json_response.FindString(kTokenStatusKey); |
| bool is_valid = false; |
| if (sync_token_status && *sync_token_status == kTokenStatusValid) { |
| is_valid = true; |
| } |
| RecordEvent(is_valid |
| ? InSessionPasswordSyncEvent::kTokenValidationSuccess |
| : InSessionPasswordSyncEvent::kTokenValidationFailure); |
| consumer_->OnTokenVerified(is_valid); |
| break; |
| } |
| case RequestType::kNone: |
| NOTREACHED(); |
| } |
| } |
| |
| } // namespace ash |