blob: 119e68097cbaf4a05b51acf74fe1b74505d5ca5d [file] [log] [blame]
// 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/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/signin/token_service.h"
#include "chrome/browser/signin/token_service_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() {
SigninManagerBase::InitTokenService();
TokenService* token_service = TokenServiceFactory::GetForProfile(profile_);
if (token_service && !GetAuthenticatedUsername().empty())
token_service->LoadTokensFromDB();
}
std::string SigninManager::SigninTypeToString(
SigninManager::SigninType type) {
switch (type) {
case SIGNIN_TYPE_NONE:
return "No Signin";
case SIGNIN_TYPE_CLIENT_LOGIN:
return "Client Login";
case SIGNIN_TYPE_WITH_CREDENTIALS:
return "Signin with credentials";
}
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.
RevokeOAuthLoginToken();
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;
}
// Users must always sign out before they sign in again.
void SigninManager::StartSignIn(const std::string& username,
const std::string& password,
const std::string& login_token,
const std::string& login_captcha) {
DCHECK(GetAuthenticatedUsername().empty() ||
gaia::AreEmailsSame(username, GetAuthenticatedUsername()));
if (!PrepareForSignin(SIGNIN_TYPE_CLIENT_LOGIN, username, password))
return;
client_login_->StartClientLogin(username,
password,
"",
login_token,
login_captcha,
GaiaAuthFetcher::HostedAccountsNotAllowed);
}
void SigninManager::ProvideSecondFactorAccessCode(
const std::string& access_code) {
DCHECK(!possibly_invalid_username_.empty() && !password_.empty() &&
last_result_.data.empty());
DCHECK(type_ == SIGNIN_TYPE_CLIENT_LOGIN);
client_login_.reset(new GaiaAuthFetcher(this,
GaiaConstants::kChromeSource,
profile_->GetRequestContext()));
client_login_->StartClientLogin(possibly_invalid_username_,
access_code,
"",
std::string(),
std::string(),
GaiaAuthFetcher::HostedAccountsNotAllowed);
}
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::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();
RevokeOAuthLoginToken();
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));
TokenService* token_service = TokenServiceFactory::GetForProfile(profile_);
token_service->ResetCredentialsInMemory();
token_service->EraseTokensFromDB();
}
void SigninManager::Initialize(Profile* profile, PrefService* local_state) {
SigninManagerBase::Initialize(profile, local_state);
// 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();
}
}
void SigninManager::Shutdown() {
local_state_pref_registrar_.RemoveAll();
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.
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;
}
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);
}
void SigninManager::RevokeOAuthLoginToken() {
TokenService* token_service = TokenServiceFactory::GetForProfile(profile_);
if (token_service->HasOAuthLoginToken()) {
revoke_token_fetcher_.reset(
new GaiaAuthFetcher(this,
GaiaConstants::kChromeSource,
profile_->GetRequestContext()));
revoke_token_fetcher_->StartRevokeOAuth2Token(
token_service->GetOAuth2LoginRefreshToken());
}
}
void SigninManager::OnOAuth2RevokeTokenCompleted() {
revoke_token_fetcher_.reset(NULL);
}
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:
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_);
TokenService* token_service = TokenServiceFactory::GetForProfile(profile_);
token_service->UpdateCredentials(last_result_);
DCHECK(token_service->AreCredentialsValid());
token_service->StartFetchingTokens();
// If we have oauth2 tokens, tell token service about them so it does not
// need to fetch them again.
if (!temp_oauth_login_tokens_.refresh_token.empty()) {
token_service->UpdateCredentialsWithOAuth2(temp_oauth_login_tokens_);
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));
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_;
}