// Copyright 2013 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/signin/oauth2_token_service_delegate_android.h" #include "base/android/context_utils.h" #include "base/android/jni_android.h" #include "base/android/jni_array.h" #include "base/android/jni_string.h" #include "base/bind.h" #include "base/logging.h" #include "base/macros.h" #include "base/metrics/histogram_macros.h" #include "chrome/browser/profiles/profile_android.h" #include "chrome/browser/signin/profile_oauth2_token_service_factory.h" #include "chrome/browser/sync/profile_sync_service_android.h" #include "components/signin/core/browser/account_info.h" #include "content/public/browser/browser_thread.h" #include "google_apis/gaia/gaia_auth_util.h" #include "google_apis/gaia/oauth2_access_token_fetcher.h" #include "jni/OAuth2TokenService_jni.h" using base::android::AttachCurrentThread; using base::android::ConvertJavaStringToUTF8; using base::android::ConvertUTF8ToJavaString; using base::android::ScopedJavaLocalRef; using content::BrowserThread; namespace { // Callback from FetchOAuth2TokenWithUsername(). // Arguments: // - the error, or NONE if the token fetch was successful. // - the OAuth2 access token. // - the expiry time of the token (may be null, indicating that the expiry // time is unknown. typedef base::Callback FetchOAuth2TokenCallback; class AndroidAccessTokenFetcher : public OAuth2AccessTokenFetcher { public: AndroidAccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, const std::string& account_id); ~AndroidAccessTokenFetcher() override; // Overrides from OAuth2AccessTokenFetcher: void Start(const std::string& client_id, const std::string& client_secret, const std::vector& scopes) override; void CancelRequest() override; // Handles an access token response. void OnAccessTokenResponse(const GoogleServiceAuthError& error, const std::string& access_token, const base::Time& expiration_time); private: std::string CombineScopes(const std::vector& scopes); std::string account_id_; bool request_was_cancelled_; base::WeakPtrFactory weak_factory_; DISALLOW_COPY_AND_ASSIGN(AndroidAccessTokenFetcher); }; AndroidAccessTokenFetcher::AndroidAccessTokenFetcher( OAuth2AccessTokenConsumer* consumer, const std::string& account_id) : OAuth2AccessTokenFetcher(consumer), account_id_(account_id), request_was_cancelled_(false), weak_factory_(this) { } AndroidAccessTokenFetcher::~AndroidAccessTokenFetcher() { } void AndroidAccessTokenFetcher::Start(const std::string& client_id, const std::string& client_secret, const std::vector& scopes) { JNIEnv* env = AttachCurrentThread(); std::string scope = CombineScopes(scopes); ScopedJavaLocalRef j_username = ConvertUTF8ToJavaString(env, account_id_); ScopedJavaLocalRef j_scope = ConvertUTF8ToJavaString(env, scope); scoped_ptr heap_callback( new FetchOAuth2TokenCallback( base::Bind(&AndroidAccessTokenFetcher::OnAccessTokenResponse, weak_factory_.GetWeakPtr()))); // Call into Java to get a new token. Java_OAuth2TokenService_getOAuth2AuthToken( env, base::android::GetApplicationContext(), j_username.obj(), j_scope.obj(), reinterpret_cast(heap_callback.release())); } void AndroidAccessTokenFetcher::CancelRequest() { request_was_cancelled_ = true; } void AndroidAccessTokenFetcher::OnAccessTokenResponse( const GoogleServiceAuthError& error, const std::string& access_token, const base::Time& expiration_time) { if (request_was_cancelled_) { // Ignore the callback if the request was cancelled. return; } if (error.state() == GoogleServiceAuthError::NONE) { FireOnGetTokenSuccess(access_token, expiration_time); } else { FireOnGetTokenFailure(error); } } // static std::string AndroidAccessTokenFetcher::CombineScopes( const std::vector& scopes) { // The Android AccountManager supports multiple scopes separated by a space: // https://code.google.com/p/google-api-java-client/wiki/OAuth2#Android std::string scope; for (std::vector::const_iterator it = scopes.begin(); it != scopes.end(); ++it) { if (!scope.empty()) scope += " "; scope += *it; } return scope; } } // namespace bool OAuth2TokenServiceDelegateAndroid::is_testing_profile_ = false; OAuth2TokenServiceDelegateAndroid::ErrorInfo::ErrorInfo() : error(GoogleServiceAuthError::NONE) {} OAuth2TokenServiceDelegateAndroid::ErrorInfo::ErrorInfo( const GoogleServiceAuthError& error) : error(error) {} OAuth2TokenServiceDelegateAndroid::OAuth2TokenServiceDelegateAndroid( AccountTrackerService* account_tracker_service) : account_tracker_service_(account_tracker_service), fire_refresh_token_loaded_(RT_LOAD_NOT_START) { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ctor"; DCHECK(account_tracker_service_); JNIEnv* env = AttachCurrentThread(); base::android::ScopedJavaLocalRef local_java_ref = Java_OAuth2TokenService_create(env, base::android::GetApplicationContext(), reinterpret_cast(this)); java_ref_.Reset(env, local_java_ref.obj()); if (account_tracker_service_->GetMigrationState() == AccountTrackerService::MIGRATION_IN_PROGRESS) { std::vector accounts = GetAccounts(); std::vector accounts_id; for (auto account_name : accounts) { AccountInfo account_info = account_tracker_service_->FindAccountInfoByEmail(account_name); DCHECK(!account_info.gaia.empty()); accounts_id.push_back(account_info.gaia); } ScopedJavaLocalRef java_accounts( base::android::ToJavaArrayOfStrings(env, accounts_id)); Java_OAuth2TokenService_saveStoredAccounts( env, base::android::GetApplicationContext(), java_accounts.obj()); } if (!is_testing_profile_) { Java_OAuth2TokenService_validateAccounts( AttachCurrentThread(), java_ref_.obj(), base::android::GetApplicationContext(), JNI_TRUE); } } OAuth2TokenServiceDelegateAndroid::~OAuth2TokenServiceDelegateAndroid() { } // static ScopedJavaLocalRef OAuth2TokenServiceDelegateAndroid::GetForProfile( JNIEnv* env, jclass clazz, jobject j_profile_android) { Profile* profile = ProfileAndroid::FromProfileAndroid(j_profile_android); ProfileOAuth2TokenService* service = ProfileOAuth2TokenServiceFactory::GetForProfile(profile); OAuth2TokenServiceDelegate* delegate = service->GetDelegate(); return ScopedJavaLocalRef( static_cast(delegate)->java_ref_); } static ScopedJavaLocalRef GetForProfile( JNIEnv* env, const JavaParamRef& clazz, const JavaParamRef& j_profile_android) { return OAuth2TokenServiceDelegateAndroid::GetForProfile(env, clazz, j_profile_android); } bool OAuth2TokenServiceDelegateAndroid::RefreshTokenIsAvailable( const std::string& account_id) const { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::RefreshTokenIsAvailable" << " account= " << account_id; std::string account_name = MapAccountIdToAccountName(account_id); JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef j_account_id = ConvertUTF8ToJavaString(env, account_name); jboolean refresh_token_is_available = Java_OAuth2TokenService_hasOAuth2RefreshToken( env, base::android::GetApplicationContext(), j_account_id.obj()); return refresh_token_is_available == JNI_TRUE; } bool OAuth2TokenServiceDelegateAndroid::RefreshTokenHasError( const std::string& account_id) const { auto it = errors_.find(account_id); // TODO(rogerta): should we distinguish between transient and persistent? return it == errors_.end() ? false : IsError(it->second.error); } void OAuth2TokenServiceDelegateAndroid::UpdateAuthError( const std::string& account_id, const GoogleServiceAuthError& error) { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::UpdateAuthError" << " account=" << account_id << " error=" << error.ToString(); if (error.state() == GoogleServiceAuthError::NONE) { errors_.erase(account_id); } else { // TODO(rogerta): should we distinguish between transient and persistent? errors_[account_id] = ErrorInfo(error); } } std::vector OAuth2TokenServiceDelegateAndroid::GetAccounts() { std::vector accounts; JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef j_accounts = Java_OAuth2TokenService_getAccounts( env, base::android::GetApplicationContext()); // TODO(fgorski): We may decide to filter out some of the accounts. base::android::AppendJavaStringArrayToStringVector(env, j_accounts.obj(), &accounts); return accounts; } std::vector OAuth2TokenServiceDelegateAndroid::GetSystemAccountNames() { std::vector account_names; JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef j_accounts = Java_OAuth2TokenService_getSystemAccountNames( env, base::android::GetApplicationContext()); base::android::AppendJavaStringArrayToStringVector(env, j_accounts.obj(), &account_names); return account_names; } OAuth2AccessTokenFetcher* OAuth2TokenServiceDelegateAndroid::CreateAccessTokenFetcher( const std::string& account_id, net::URLRequestContextGetter* getter, OAuth2AccessTokenConsumer* consumer) { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::CreateAccessTokenFetcher" << " account= " << account_id; ValidateAccountId(account_id); return new AndroidAccessTokenFetcher(consumer, MapAccountIdToAccountName(account_id)); } void OAuth2TokenServiceDelegateAndroid::InvalidateAccessToken( const std::string& account_id, const std::string& client_id, const OAuth2TokenService::ScopeSet& scopes, const std::string& access_token) { ValidateAccountId(account_id); JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef j_access_token = ConvertUTF8ToJavaString(env, access_token); Java_OAuth2TokenService_invalidateOAuth2AuthToken( env, base::android::GetApplicationContext(), j_access_token.obj()); } void OAuth2TokenServiceDelegateAndroid::ValidateAccounts( JNIEnv* env, const JavaParamRef& obj, const JavaParamRef& j_current_acc, jboolean j_force_notifications) { std::string signed_in_account_name; DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ValidateAccounts from java"; if (j_current_acc) signed_in_account_name = ConvertJavaStringToUTF8(env, j_current_acc); if (!signed_in_account_name.empty()) signed_in_account_name = gaia::CanonicalizeEmail(signed_in_account_name); // Clear any auth errors so that client can retry to get access tokens. errors_.clear(); ValidateAccounts(MapAccountNameToAccountId(signed_in_account_name), j_force_notifications != JNI_FALSE); } void OAuth2TokenServiceDelegateAndroid::ValidateAccounts( const std::string& signed_in_account_id, bool force_notifications) { std::vector curr_ids; for (const std::string& curr_name : GetSystemAccountNames()) { std::string curr_id(MapAccountNameToAccountId(curr_name)); if (!curr_id.empty()) curr_ids.push_back(curr_id); } std::vector prev_ids; for (const std::string& prev_id : GetAccounts()) { if (ValidateAccountId(prev_id)) prev_ids.push_back(prev_id); } DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ValidateAccounts:" << " sigined_in_account_id=" << signed_in_account_id << " prev_ids=" << prev_ids.size() << " curr_ids=" << curr_ids.size() << " force=" << (force_notifications ? "true" : "false"); std::vector refreshed_ids; std::vector revoked_ids; bool currently_signed_in = ValidateAccounts(signed_in_account_id, prev_ids, curr_ids, &refreshed_ids, &revoked_ids, force_notifications); ScopedBatchChange batch(this); JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef java_accounts; if (currently_signed_in) { java_accounts = base::android::ToJavaArrayOfStrings(env, curr_ids); } else { java_accounts = base::android::ToJavaArrayOfStrings(env, std::vector()); } // Save the current accounts in the token service before calling // FireRefreshToken* methods. Java_OAuth2TokenService_saveStoredAccounts( env, base::android::GetApplicationContext(), java_accounts.obj()); for (const std::string& refreshed_id : refreshed_ids) FireRefreshTokenAvailable(refreshed_id); for (const std::string& revoked_id : revoked_ids) FireRefreshTokenRevoked(revoked_id); if (fire_refresh_token_loaded_ == RT_WAIT_FOR_VALIDATION) { fire_refresh_token_loaded_ = RT_LOADED; FireRefreshTokensLoaded(); } else if (fire_refresh_token_loaded_ == RT_LOAD_NOT_START) { fire_refresh_token_loaded_ = RT_HAS_BEEN_VALIDATED; } // Clear accounts no longer exist on device from AccountTrackerService. std::vector accounts_info = account_tracker_service_->GetAccounts(); for (const AccountInfo& info : accounts_info) { if (std::find(curr_ids.begin(), curr_ids.end(), info.account_id) == curr_ids.end()) account_tracker_service_->RemoveAccount(info.account_id); } // No need to wait for SigninManager to finish migration if not signed in. if (account_tracker_service_->GetMigrationState() == AccountTrackerService::MIGRATION_IN_PROGRESS && signed_in_account_id.empty()) { account_tracker_service_->SetMigrationDone(); } } bool OAuth2TokenServiceDelegateAndroid::ValidateAccounts( const std::string& signed_in_id, const std::vector& prev_ids, const std::vector& curr_ids, std::vector* refreshed_ids, std::vector* revoked_ids, bool force_notifications) { bool currently_signed_in = std::find(curr_ids.begin(), curr_ids.end(), signed_in_id) != curr_ids.end(); if (currently_signed_in) { // Revoke token for ids that have been removed from the device. for (const std::string& prev_id : prev_ids) { if (prev_id == signed_in_id) continue; if (std::find(curr_ids.begin(), curr_ids.end(), prev_id) == curr_ids.end()) { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ValidateAccounts:" << "revoked=" << prev_id; revoked_ids->push_back(prev_id); } } // Refresh token for new ids or all ids if |force_notifications|. if (force_notifications || std::find(prev_ids.begin(), prev_ids.end(), signed_in_id) == prev_ids.end()) { // Always fire the primary signed in account first. DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ValidateAccounts:" << "refreshed=" << signed_in_id; refreshed_ids->push_back(signed_in_id); } for (const std::string& curr_id : curr_ids) { if (curr_id == signed_in_id) continue; if (force_notifications || std::find(prev_ids.begin(), prev_ids.end(), curr_id) == prev_ids.end()) { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ValidateAccounts:" << "refreshed=" << curr_id; refreshed_ids->push_back(curr_id); } } } else { if (std::find(prev_ids.begin(), prev_ids.end(), signed_in_id) != prev_ids.end()) { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ValidateAccounts:" << "revoked=" << signed_in_id; revoked_ids->push_back(signed_in_id); } for (const std::string& prev_id : prev_ids) { if (prev_id == signed_in_id) continue; DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::ValidateAccounts:" << "revoked=" << prev_id; revoked_ids->push_back(prev_id); } } return currently_signed_in; } void OAuth2TokenServiceDelegateAndroid::FireRefreshTokenAvailableFromJava( JNIEnv* env, const JavaParamRef& obj, const JavaParamRef& account_name) { std::string account_id = MapAccountNameToAccountId(ConvertJavaStringToUTF8(env, account_name)); // Notify native observers. FireRefreshTokenAvailable(account_id); } void OAuth2TokenServiceDelegateAndroid::FireRefreshTokenAvailable( const std::string& account_id) { // TODO(knn): Convert to DCHECK after https://crbug.com/535211 CHECK(!account_id.empty()); DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::FireRefreshTokenAvailable id=" << account_id; std::string account_name = MapAccountIdToAccountName(account_id); // TODO(knn): Convert to DCHECK after https://crbug.com/535211 CHECK(!account_name.empty()); JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef j_account_name = ConvertUTF8ToJavaString(env, account_name); Java_OAuth2TokenService_notifyRefreshTokenAvailable(env, java_ref_.obj(), j_account_name.obj()); OAuth2TokenServiceDelegate::FireRefreshTokenAvailable(account_id); } void OAuth2TokenServiceDelegateAndroid::FireRefreshTokenRevokedFromJava( JNIEnv* env, const JavaParamRef& obj, const JavaParamRef& account_name) { std::string account_id = MapAccountNameToAccountId(ConvertJavaStringToUTF8(env, account_name)); // Notify native observers. FireRefreshTokenRevoked(account_id); } void OAuth2TokenServiceDelegateAndroid::FireRefreshTokenRevoked( const std::string& account_id) { // TODO(knn): Convert to DCHECK after https://crbug.com/535211 CHECK(!account_id.empty()); DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::FireRefreshTokenRevoked id=" << account_id; std::string account_name = MapAccountIdToAccountName(account_id); if (!account_name.empty()) { JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef j_account_name = ConvertUTF8ToJavaString(env, account_name); Java_OAuth2TokenService_notifyRefreshTokenRevoked(env, java_ref_.obj(), j_account_name.obj()); } else { // Current prognosis is that we have an unmigrated account which is due for // deletion. Record a histogram to debug this. UMA_HISTOGRAM_ENUMERATION("OAuth2Login.AccountRevoked.MigrationState", account_tracker_service_->GetMigrationState(), AccountTrackerService::NUM_MIGRATION_STATES); bool is_email_id = account_id.find('@') != std::string::npos; UMA_HISTOGRAM_BOOLEAN("OAuth2Login.AccountRevoked.IsEmailId", is_email_id); } OAuth2TokenServiceDelegate::FireRefreshTokenRevoked(account_id); } void OAuth2TokenServiceDelegateAndroid::FireRefreshTokensLoadedFromJava( JNIEnv* env, const JavaParamRef& obj) { // Notify native observers. FireRefreshTokensLoaded(); } void OAuth2TokenServiceDelegateAndroid::FireRefreshTokensLoaded() { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::FireRefreshTokensLoaded"; JNIEnv* env = AttachCurrentThread(); Java_OAuth2TokenService_notifyRefreshTokensLoaded(env, java_ref_.obj()); OAuth2TokenServiceDelegate::FireRefreshTokensLoaded(); } void OAuth2TokenServiceDelegateAndroid::RevokeAllCredentials() { DVLOG(1) << "OAuth2TokenServiceDelegateAndroid::RevokeAllCredentials"; ScopedBatchChange batch(this); std::vector accounts_to_revoke = GetAccounts(); // Clear accounts in the token service before calling // |FireRefreshTokenRevoked|. JNIEnv* env = AttachCurrentThread(); ScopedJavaLocalRef java_accounts( base::android::ToJavaArrayOfStrings(env, std::vector())); Java_OAuth2TokenService_saveStoredAccounts( env, base::android::GetApplicationContext(), java_accounts.obj()); for (const std::string& account : accounts_to_revoke) FireRefreshTokenRevoked(account); } void OAuth2TokenServiceDelegateAndroid::LoadCredentials( const std::string& primary_account_id) { if (primary_account_id.empty()) { FireRefreshTokensLoaded(); return; } if (fire_refresh_token_loaded_ == RT_HAS_BEEN_VALIDATED) { fire_refresh_token_loaded_ = RT_LOADED; FireRefreshTokensLoaded(); } else if (fire_refresh_token_loaded_ == RT_LOAD_NOT_START) { fire_refresh_token_loaded_ = RT_WAIT_FOR_VALIDATION; } } std::string OAuth2TokenServiceDelegateAndroid::MapAccountIdToAccountName( const std::string& account_id) const { std::string account_name = account_tracker_service_->GetAccountInfo(account_id).email; DCHECK(!account_name.empty() || account_id.empty()); return account_name; } std::string OAuth2TokenServiceDelegateAndroid::MapAccountNameToAccountId( const std::string& account_name) const { std::string account_id = account_tracker_service_->FindAccountInfoByEmail(account_name).account_id; DCHECK(!account_id.empty() || account_name.empty()); return account_id; } // Called from Java when fetching of an OAuth2 token is finished. The // |authToken| param is only valid when |result| is true. void OAuth2TokenFetched(JNIEnv* env, const JavaParamRef& clazz, const JavaParamRef& authToken, jboolean isTransientError, jlong nativeCallback) { std::string token; if (authToken) token = ConvertJavaStringToUTF8(env, authToken); scoped_ptr heap_callback( reinterpret_cast(nativeCallback)); GoogleServiceAuthError err(authToken ? GoogleServiceAuthError::NONE : isTransientError ? GoogleServiceAuthError::CONNECTION_FAILED : GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); heap_callback->Run(err, token, base::Time()); } // static bool OAuth2TokenServiceDelegateAndroid::Register(JNIEnv* env) { return RegisterNativesImpl(env); }