普通文本  |  630行  |  22.81 KB

// Copyright (c) 2012 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/signin_manager.h"

#include <string>
#include <vector>

#include "base/command_line.h"
#include "base/memory/ref_counted.h"
#include "base/prefs/pref_service.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/profiles/profile_io_data.h"
#include "chrome/browser/signin/about_signin_internals.h"
#include "chrome/browser/signin/about_signin_internals_factory.h"
#include "chrome/browser/signin/local_auth.h"
#include "chrome/browser/signin/profile_oauth2_token_service.h"
#include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
#include "chrome/browser/signin/signin_account_id_helper.h"
#include "chrome/browser/signin/signin_global_error.h"
#include "chrome/browser/signin/signin_internals_util.h"
#include "chrome/browser/signin/signin_manager_cookie_helper.h"
#include "chrome/browser/signin/signin_manager_delegate.h"
#include "chrome/browser/signin/signin_manager_factory.h"
#include "chrome/browser/ui/global_error/global_error_service.h"
#include "chrome/browser/ui/global_error/global_error_service_factory.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_process_host.h"
#include "google_apis/gaia/gaia_auth_fetcher.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/base/escape.h"
#include "net/url_request/url_request_context.h"
#include "third_party/icu/source/i18n/unicode/regex.h"

using namespace signin_internals_util;

using content::BrowserThread;

namespace {

const char kGetInfoDisplayEmailKey[] = "displayEmail";
const char kGetInfoEmailKey[] = "email";

const int kInvalidProcessId = -1;

const char kChromiumSyncService[] = "service=chromiumsync";

}  // namespace

// Under the covers, we use a dummy chrome-extension ID to serve the purposes
// outlined in the .h file comment for this string.
const char* SigninManager::kChromeSigninEffectiveSite =
    "chrome-extension://acfccoigjajmmgbhpfbjnpckhjjegnih";

// static
bool SigninManager::IsWebBasedSigninFlowURL(const GURL& url) {
  GURL effective(kChromeSigninEffectiveSite);
  if (url.SchemeIs(effective.scheme().c_str()) &&
      url.host() == effective.host()) {
    return true;
  }

  GURL service_login(GaiaUrls::GetInstance()->service_login_url());
  if (url.GetOrigin() != service_login.GetOrigin())
    return false;

  // Any login UI URLs with signin=chromiumsync should be considered a web
  // URL (relies on GAIA keeping the "service=chromiumsync" query string
  // fragment present even when embedding inside a "continue" parameter).
  return net::UnescapeURLComponent(
      url.query(), net::UnescapeRule::URL_SPECIAL_CHARS)
          .find(kChromiumSyncService) != std::string::npos;
}

SigninManager::SigninManager(scoped_ptr<SigninManagerDelegate> delegate)
    : prohibit_signout_(false),
      had_two_factor_error_(false),
      type_(SIGNIN_TYPE_NONE),
      weak_pointer_factory_(this),
      signin_process_id_(kInvalidProcessId),
      delegate_(delegate.Pass()) {
}

void SigninManager::SetSigninProcess(int process_id) {
  if (process_id == signin_process_id_)
    return;
  DLOG_IF(WARNING, signin_process_id_ != kInvalidProcessId) <<
      "Replacing in-use signin process.";
  signin_process_id_ = process_id;
  const content::RenderProcessHost* process =
      content::RenderProcessHost::FromID(process_id);
  DCHECK(process);
  registrar_.Add(this,
                 content::NOTIFICATION_RENDERER_PROCESS_TERMINATED,
                 content::Source<content::RenderProcessHost>(process));
}

void SigninManager::ClearSigninProcess() {
  signin_process_id_ = kInvalidProcessId;
}

bool SigninManager::IsSigninProcess(int process_id) const {
  return process_id == signin_process_id_;
}

bool SigninManager::HasSigninProcess() const {
  return signin_process_id_ != kInvalidProcessId;
}

SigninManager::~SigninManager() {
}

void SigninManager::InitTokenService() {
  ProfileOAuth2TokenService* token_service =
      ProfileOAuth2TokenServiceFactory::GetForProfile(profile_);
  if (token_service && !GetAuthenticatedUsername().empty())
    token_service->LoadCredentials();
}

std::string SigninManager::SigninTypeToString(
    SigninManager::SigninType type) {
  switch (type) {
    case SIGNIN_TYPE_NONE:
      return "No Signin";
    case SIGNIN_TYPE_WITH_CREDENTIALS:
      return "Signin with credentials";
    case SIGNIN_TYPE_WITH_OAUTH_CODE:
      return "Signin with oauth code";
  }

  NOTREACHED();
  return std::string();
}

bool SigninManager::PrepareForSignin(SigninType type,
                                     const std::string& username,
                                     const std::string& password) {
  DCHECK(possibly_invalid_username_.empty() ||
         possibly_invalid_username_ == username);
  DCHECK(!username.empty());

  if (!IsAllowedUsername(username)) {
    // Account is not allowed by admin policy.
    HandleAuthError(GoogleServiceAuthError(
        GoogleServiceAuthError::ACCOUNT_DISABLED), true);
    return false;
  }

  // This attempt is either 1) the user trying to establish initial sync, or
  // 2) trying to refresh credentials for an existing username.  If it is 2, we
  // need to try again, but take care to leave state around tracking that the
  // user has successfully signed in once before with this username, so that on
  // restart we don't think sync setup has never completed.
  ClearTransientSigninData();
  type_ = type;
  possibly_invalid_username_.assign(username);
  password_.assign(password);

  client_login_.reset(new GaiaAuthFetcher(this,
                                          GaiaConstants::kChromeSource,
                                          profile_->GetRequestContext()));
  NotifyDiagnosticsObservers(SIGNIN_TYPE, SigninTypeToString(type));
  return true;
}

void SigninManager::StartSignInWithCredentials(
    const std::string& session_index,
    const std::string& username,
    const std::string& password,
    const OAuthTokenFetchedCallback& callback) {
  DCHECK(GetAuthenticatedUsername().empty() ||
         gaia::AreEmailsSame(username, GetAuthenticatedUsername()));

  if (!PrepareForSignin(SIGNIN_TYPE_WITH_CREDENTIALS, username, password))
    return;

  // Store our callback.
  DCHECK(oauth_token_fetched_callback_.is_null());
  oauth_token_fetched_callback_ = callback;

  if (password.empty()) {
    // Chrome must verify the GAIA cookies first if auto sign-in is triggered
    // with no password provided. This is to protect Chrome against forged
    // GAIA cookies from a super-domain.
    VerifyGaiaCookiesBeforeSignIn(session_index);
  } else {
    // This function starts with the current state of the web session's cookie
    // jar and mints a new ClientLogin-style SID/LSID pair.  This involves going
    // through the follow process or requests to GAIA and LSO:
    //
    // - call /o/oauth2/programmatic_auth with the returned token to get oauth2
    //   access and refresh tokens
    // - call /accounts/OAuthLogin with the oauth2 access token and get SID/LSID
    //   pair for use by the token service
    //
    // The resulting SID/LSID can then be used just as if
    // client_login_->StartClientLogin() had completed successfully.
    client_login_->StartCookieForOAuthLoginTokenExchange(session_index);
  }
}

void SigninManager::StartSignInWithOAuthCode(
    const std::string& username,
    const std::string& password,
    const std::string& oauth_code,
    const OAuthTokenFetchedCallback& callback) {
  DCHECK(GetAuthenticatedUsername().empty() ||
         gaia::AreEmailsSame(username, GetAuthenticatedUsername()));

  if (!PrepareForSignin(SIGNIN_TYPE_WITH_OAUTH_CODE, username, password))
    return;

  DCHECK(oauth_token_fetched_callback_.is_null());
  oauth_token_fetched_callback_ = callback;

  client_login_->StartAuthCodeForOAuth2TokenExchange(oauth_code);
}

void SigninManager::VerifyGaiaCookiesBeforeSignIn(
    const std::string& session_index) {
  scoped_refptr<SigninManagerCookieHelper> cookie_helper(
      new SigninManagerCookieHelper(profile_->GetRequestContext()));
  cookie_helper->StartFetchingGaiaCookiesOnUIThread(
      base::Bind(&SigninManager::OnGaiaCookiesFetched,
                 weak_pointer_factory_.GetWeakPtr(), session_index));
}

void SigninManager::OnGaiaCookiesFetched(
    const std::string session_index, const net::CookieList& cookie_list) {
  net::CookieList::const_iterator it;
  bool success = false;
  for (it = cookie_list.begin(); it != cookie_list.end(); ++it) {
    // Make sure the LSID cookie is set on the GAIA host, instead of a super-
    // domain.
    if (it->Name() == "LSID") {
      if (it->IsHostCookie() && it->IsHttpOnly() && it->IsSecure()) {
        // Found a valid LSID cookie. Continue loop to make sure we don't have
        // invalid LSID cookies on any super-domain.
        success = true;
      } else {
        success = false;
        break;
      }
    }
  }

  if (success) {
    client_login_->StartCookieForOAuthLoginTokenExchange(session_index);
  } else {
    HandleAuthError(GoogleServiceAuthError(
        GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS), true);
  }
}

void SigninManager::CopyCredentialsFrom(const SigninManager& source) {
  DCHECK_NE(this, &source);
  possibly_invalid_username_ = source.possibly_invalid_username_;
  last_result_ = source.last_result_;
  temp_oauth_login_tokens_ = source.temp_oauth_login_tokens_;
}

void SigninManager::ClearTransientSigninData() {
  DCHECK(IsInitialized());

  client_login_.reset();
  last_result_ = ClientLoginResult();
  possibly_invalid_username_.clear();
  password_.clear();
  had_two_factor_error_ = false;
  type_ = SIGNIN_TYPE_NONE;
  temp_oauth_login_tokens_ = ClientOAuthResult();
  oauth_token_fetched_callback_.Reset();
}

void SigninManager::HandleAuthError(const GoogleServiceAuthError& error,
                                    bool clear_transient_data) {
  // In some cases, the user should not be signed out.  For example, the failure
  // may be due to a captcha or OTP challenge.  In these cases, the transient
  // data must be kept to properly handle the follow up. This routine clears
  // the data before sending out the notification so the SigninManager is no
  // longer in the AuthInProgress state when the notification goes out.
  if (clear_transient_data)
    ClearTransientSigninData();

  content::NotificationService::current()->Notify(
      chrome::NOTIFICATION_GOOGLE_SIGNIN_FAILED,
      content::Source<Profile>(profile_),
      content::Details<const GoogleServiceAuthError>(&error));
}

void SigninManager::SignOut() {
  DCHECK(IsInitialized());

  if (GetAuthenticatedUsername().empty()) {
    if (AuthInProgress()) {
      // If the user is in the process of signing in, then treat a call to
      // SignOut as a cancellation request.
      GoogleServiceAuthError error(GoogleServiceAuthError::REQUEST_CANCELED);
      HandleAuthError(error, true);
    } else {
      // Clean up our transient data and exit if we aren't signed in.
      // This avoids a perf regression from clearing out the TokenDB if
      // SignOut() is invoked on startup to clean up any incomplete previous
      // signin attempts.
      ClearTransientSigninData();
    }
    return;
  }

  if (prohibit_signout_) {
    DVLOG(1) << "Ignoring attempt to sign out while signout is prohibited";
    return;
  }

  ClearTransientSigninData();

  GoogleServiceSignoutDetails details(GetAuthenticatedUsername());
  clear_authenticated_username();
  profile_->GetPrefs()->ClearPref(prefs::kGoogleServicesUsername);

  // Erase (now) stale information from AboutSigninInternals.
  NotifyDiagnosticsObservers(USERNAME, "");

  content::NotificationService::current()->Notify(
      chrome::NOTIFICATION_GOOGLE_SIGNED_OUT,
      content::Source<Profile>(profile_),
      content::Details<const GoogleServiceSignoutDetails>(&details));
  ProfileOAuth2TokenService* token_service =
      ProfileOAuth2TokenServiceFactory::GetForProfile(profile_);
  token_service->RevokeAllCredentials();
}

void SigninManager::Initialize(Profile* profile, PrefService* local_state) {
  SigninManagerBase::Initialize(profile, local_state);

  InitTokenService();

  // local_state can be null during unit tests.
  if (local_state) {
    local_state_pref_registrar_.Init(local_state);
    local_state_pref_registrar_.Add(
        prefs::kGoogleServicesUsernamePattern,
        base::Bind(&SigninManager::OnGoogleServicesUsernamePatternChanged,
                   weak_pointer_factory_.GetWeakPtr()));
  }
  signin_allowed_.Init(prefs::kSigninAllowed, profile_->GetPrefs(),
      base::Bind(&SigninManager::OnSigninAllowedPrefChanged,
                 base::Unretained(this)));

  std::string user = profile_->GetPrefs()->GetString(
      prefs::kGoogleServicesUsername);
  if ((!user.empty() && !IsAllowedUsername(user)) || !IsSigninAllowed()) {
    // User is signed in, but the username is invalid - the administrator must
    // have changed the policy since the last signin, so sign out the user.
    SignOut();
  }

  account_id_helper_.reset(new SigninAccountIdHelper(profile));
}

void SigninManager::Shutdown() {
  local_state_pref_registrar_.RemoveAll();
  account_id_helper_.reset();
  SigninManagerBase::Shutdown();
}

void SigninManager::OnGoogleServicesUsernamePatternChanged() {
  if (!GetAuthenticatedUsername().empty() &&
      !IsAllowedUsername(GetAuthenticatedUsername())) {
    // Signed in user is invalid according to the current policy so sign
    // the user out.
    SignOut();
  }
}

bool SigninManager::IsSigninAllowed() const {
  return signin_allowed_.GetValue();
}

// static
bool SigninManager::IsSigninAllowedOnIOThread(ProfileIOData* io_data) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::IO));
  return io_data->signin_allowed()->GetValue();
}

void SigninManager::OnSigninAllowedPrefChanged() {
  if (!IsSigninAllowed())
    SignOut();
}

// static
bool SigninManager::IsUsernameAllowedByPolicy(const std::string& username,
                                              const std::string& policy) {
  if (policy.empty())
    return true;

  // Patterns like "*@foo.com" are not accepted by our regex engine (since they
  // are not valid regular expressions - they should instead be ".*@foo.com").
  // For convenience, detect these patterns and insert a "." character at the
  // front.
  base::string16 pattern = UTF8ToUTF16(policy);
  if (pattern[0] == L'*')
    pattern.insert(pattern.begin(), L'.');

  // See if the username matches the policy-provided pattern.
  UErrorCode status = U_ZERO_ERROR;
  const icu::UnicodeString icu_pattern(pattern.data(), pattern.length());
  icu::RegexMatcher matcher(icu_pattern, UREGEX_CASE_INSENSITIVE, status);
  if (!U_SUCCESS(status)) {
    LOG(ERROR) << "Invalid login regex: " << pattern << ", status: " << status;
    // If an invalid pattern is provided, then prohibit *all* logins (better to
    // break signin than to quietly allow users to sign in).
    return false;
  }
  base::string16 username16 = UTF8ToUTF16(username);
  icu::UnicodeString icu_input(username16.data(), username16.length());
  matcher.reset(icu_input);
  status = U_ZERO_ERROR;
  UBool match = matcher.matches(status);
  DCHECK(U_SUCCESS(status));
  return !!match;  // !! == convert from UBool to bool.
}

bool SigninManager::IsAllowedUsername(const std::string& username) const {
  const PrefService* local_state = local_state_pref_registrar_.prefs();
  if (!local_state)
    return true; // In a unit test with no local state - all names are allowed.

  std::string pattern = local_state->GetString(
      prefs::kGoogleServicesUsernamePattern);
  return IsUsernameAllowedByPolicy(username, pattern);
}

bool SigninManager::AuthInProgress() const {
  return !possibly_invalid_username_.empty();
}

const std::string& SigninManager::GetUsernameForAuthInProgress() const {
  return possibly_invalid_username_;
}

void SigninManager::OnGetUserInfoKeyNotFound(const std::string& key) {
  DCHECK(key == kGetInfoDisplayEmailKey || key == kGetInfoEmailKey);
  LOG(ERROR) << "Account is not associated with a valid email address. "
             << "Login failed.";
  OnClientLoginFailure(GoogleServiceAuthError(
      GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS));
}

void SigninManager::DisableOneClickSignIn(Profile* profile) {
  PrefService* pref_service = profile->GetPrefs();
  pref_service->SetBoolean(prefs::kReverseAutologinEnabled, false);
}

void SigninManager::OnClientLoginSuccess(const ClientLoginResult& result) {
  last_result_ = result;
  // Update signin_internals_
  NotifyDiagnosticsObservers(CLIENT_LOGIN_STATUS, "Successful");
  // Make a request for the canonical email address and services.
  client_login_->StartGetUserInfo(result.lsid);
}

void SigninManager::OnClientLoginFailure(const GoogleServiceAuthError& error) {
  // If we got a bad ASP, prompt for an ASP again by forcing another TWO_FACTOR
  // error.  This function does not call HandleAuthError() because dealing
  // with TWO_FACTOR errors needs special handling: we don't want to clear the
  // transient signin data in such error cases.
  bool invalid_gaia = error.state() ==
      GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS;

  GoogleServiceAuthError current_error =
      (invalid_gaia && had_two_factor_error_) ?
      GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR) : error;

  if (current_error.state() == GoogleServiceAuthError::TWO_FACTOR)
    had_two_factor_error_ = true;

  NotifyDiagnosticsObservers(CLIENT_LOGIN_STATUS, error.ToString());
  HandleAuthError(current_error, !had_two_factor_error_);
}

void SigninManager::OnClientOAuthSuccess(const ClientOAuthResult& result) {
  DVLOG(1) << "SigninManager::OnClientOAuthSuccess access_token="
           << result.access_token;

  NotifyDiagnosticsObservers(OAUTH_LOGIN_STATUS, "Successful");

  switch (type_) {
    case SIGNIN_TYPE_WITH_CREDENTIALS:
    case SIGNIN_TYPE_WITH_OAUTH_CODE:
      temp_oauth_login_tokens_ = result;
      client_login_->StartOAuthLogin(result.access_token,
                                     GaiaConstants::kGaiaService);
      break;
    default:
      NOTREACHED();
      break;
  }
}

void SigninManager::OnClientOAuthFailure(const GoogleServiceAuthError& error) {
  bool clear_transient_data = true;
  NotifyDiagnosticsObservers(OAUTH_LOGIN_STATUS, error.ToString());
  LOG(WARNING) << "SigninManager::OnClientOAuthFailure";
  HandleAuthError(error, clear_transient_data);
}

void SigninManager::OnGetUserInfoSuccess(const UserInfoMap& data) {
  NotifyDiagnosticsObservers(GET_USER_INFO_STATUS, "Successful");

  UserInfoMap::const_iterator email_iter = data.find(kGetInfoEmailKey);
  UserInfoMap::const_iterator display_email_iter =
      data.find(kGetInfoDisplayEmailKey);
  if (email_iter == data.end()) {
    OnGetUserInfoKeyNotFound(kGetInfoEmailKey);
    return;
  }
  if (display_email_iter == data.end()) {
    OnGetUserInfoKeyNotFound(kGetInfoDisplayEmailKey);
    return;
  }
  DCHECK(email_iter->first == kGetInfoEmailKey);
  DCHECK(display_email_iter->first == kGetInfoDisplayEmailKey);

  // When signing in with credentials, the possibly invalid name is the Gaia
  // display name. If the name returned by GetUserInfo does not match what is
  // expected, return an error.
  if (type_ == SIGNIN_TYPE_WITH_CREDENTIALS &&
      !gaia::AreEmailsSame(display_email_iter->second,
                           possibly_invalid_username_)) {
    OnGetUserInfoKeyNotFound(kGetInfoDisplayEmailKey);
    return;
  }

  possibly_invalid_username_ = email_iter->second;

  if (!oauth_token_fetched_callback_.is_null() &&
      !temp_oauth_login_tokens_.refresh_token.empty()) {
    oauth_token_fetched_callback_.Run(temp_oauth_login_tokens_.refresh_token);
  } else {
    // No oauth token or callback, so just complete our pending signin.
    CompletePendingSignin();
  }
}

void SigninManager::CompletePendingSignin() {
  DCHECK(!possibly_invalid_username_.empty());
  OnSignedIn(possibly_invalid_username_);

  DCHECK(!temp_oauth_login_tokens_.refresh_token.empty());
  DCHECK(!GetAuthenticatedUsername().empty());
  ProfileOAuth2TokenService* token_service =
    ProfileOAuth2TokenServiceFactory::GetForProfile(profile_);
  token_service->UpdateCredentials(GetAuthenticatedUsername(),
                                   temp_oauth_login_tokens_.refresh_token);
  temp_oauth_login_tokens_ = ClientOAuthResult();
}

void SigninManager::OnExternalSigninCompleted(const std::string& username) {
  OnSignedIn(username);
}

void SigninManager::OnSignedIn(const std::string& username) {
  SetAuthenticatedUsername(username);
  possibly_invalid_username_.clear();
  profile_->GetPrefs()->SetString(prefs::kGoogleServicesUsername,
                                  GetAuthenticatedUsername());

  GoogleServiceSigninSuccessDetails details(GetAuthenticatedUsername(),
                                            password_);
  content::NotificationService::current()->Notify(
      chrome::NOTIFICATION_GOOGLE_SIGNIN_SUCCESSFUL,
      content::Source<Profile>(profile_),
      content::Details<const GoogleServiceSigninSuccessDetails>(&details));

#if !defined(OS_ANDROID)
  // Don't store password hash except for users of new profile features.
  if (CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kNewProfileManagement)) {
    chrome::SetLocalAuthCredentials(profile_, password_);
  }
#endif

  password_.clear();  // Don't need it anymore.
  DisableOneClickSignIn(profile_);  // Don't ever offer again.
}

void SigninManager::OnGetUserInfoFailure(const GoogleServiceAuthError& error) {
  LOG(ERROR) << "Unable to retreive the canonical email address. Login failed.";
  NotifyDiagnosticsObservers(GET_USER_INFO_STATUS, error.ToString());
  // REVIEW: why does this call OnClientLoginFailure?
  OnClientLoginFailure(error);
}

void SigninManager::Observe(int type,
                            const content::NotificationSource& source,
                            const content::NotificationDetails& details) {
  DCHECK_EQ(content::NOTIFICATION_RENDERER_PROCESS_TERMINATED, type);

  // It's possible we're listening to a "stale" renderer because it was
  // replaced with a new process by process-per-site. In either case,
  // stop listening to it, but only reset signin_process_id_ tracking
  // if this was from the current signin process.
  registrar_.Remove(this,
                    content::NOTIFICATION_RENDERER_PROCESS_TERMINATED,
                    source);
  if (signin_process_id_ ==
      content::Source<content::RenderProcessHost>(source)->GetID()) {
    signin_process_id_ = kInvalidProcessId;
  }
}

void SigninManager::ProhibitSignout(bool prohibit_signout) {
  prohibit_signout_ = prohibit_signout;
}

bool SigninManager::IsSignoutProhibited() const {
  return prohibit_signout_;
}