diff options
author | joi@chromium.org <joi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-09-07 04:26:37 +0000 |
---|---|---|
committer | joi@chromium.org <joi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-09-07 04:26:37 +0000 |
commit | 6386cf58a85361fa20bba6ecfc23502e922b9a90 (patch) | |
tree | 06f9a9aa7e6bb8db149b53bc7489e69fa87c07fb /google_apis/gaia | |
parent | f2857ecf6c8da488feeca9b05cf2c046ea43e02f (diff) | |
download | chromium_src-6386cf58a85361fa20bba6ecfc23502e922b9a90.zip chromium_src-6386cf58a85361fa20bba6ecfc23502e922b9a90.tar.gz chromium_src-6386cf58a85361fa20bba6ecfc23502e922b9a90.tar.bz2 |
Moving google_apis and GaiaClient to src/google_apis.
TBR=mechanicalowners@chromium.org
BUG=145584
Review URL: https://chromiumcodereview.appspot.com/10928017
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@155312 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'google_apis/gaia')
47 files changed, 9073 insertions, 0 deletions
diff --git a/google_apis/gaia/DEPS b/google_apis/gaia/DEPS new file mode 100644 index 0000000..6fb8e99 --- /dev/null +++ b/google_apis/gaia/DEPS @@ -0,0 +1,6 @@ +specific_include_rules = { + ".*_[a-z]*test\.cc": [ + "+chrome/test/base/testing_profile.h", + "+content/public/test/test_browser_thread.h", + ] +} diff --git a/google_apis/gaia/OWNERS b/google_apis/gaia/OWNERS new file mode 100644 index 0000000..654a183 --- /dev/null +++ b/google_apis/gaia/OWNERS @@ -0,0 +1,3 @@ +rogerta@chromium.org +tim@chromium.org +zelidrag@chromium.org diff --git a/google_apis/gaia/gaia_auth_consumer.cc b/google_apis/gaia/gaia_auth_consumer.cc new file mode 100644 index 0000000..d461bf8 --- /dev/null +++ b/google_apis/gaia/gaia_auth_consumer.cc @@ -0,0 +1,48 @@ +// 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 "google_apis/gaia/gaia_auth_consumer.h" + +GaiaAuthConsumer::ClientLoginResult::ClientLoginResult() + : two_factor(false) { +} + +GaiaAuthConsumer::ClientLoginResult::ClientLoginResult( + const std::string& new_sid, + const std::string& new_lsid, + const std::string& new_token, + const std::string& new_data) + : sid(new_sid), + lsid(new_lsid), + token(new_token), + data(new_data), + two_factor(false) {} + +GaiaAuthConsumer::ClientLoginResult::~ClientLoginResult() {} + +bool GaiaAuthConsumer::ClientLoginResult::operator==( + const ClientLoginResult &b) const { + return sid == b.sid && + lsid == b.lsid && + token == b.token && + data == b.data && + two_factor == b.two_factor; +} + +GaiaAuthConsumer::ClientOAuthResult::ClientOAuthResult( + const std::string& new_refresh_token, + const std::string& new_access_token, + int new_expires_in_secs) + : refresh_token(new_refresh_token), + access_token(new_access_token), + expires_in_secs(new_expires_in_secs) {} + +GaiaAuthConsumer::ClientOAuthResult::~ClientOAuthResult() {} + +bool GaiaAuthConsumer::ClientOAuthResult::operator==( + const ClientOAuthResult &b) const { + return refresh_token == b.refresh_token && + access_token == b.access_token && + expires_in_secs == b.expires_in_secs; +} diff --git a/google_apis/gaia/gaia_auth_consumer.h b/google_apis/gaia/gaia_auth_consumer.h new file mode 100644 index 0000000..aa84439 --- /dev/null +++ b/google_apis/gaia/gaia_auth_consumer.h @@ -0,0 +1,85 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_GAIA_AUTH_CONSUMER_H_ +#define GOOGLE_APIS_GAIA_GAIA_AUTH_CONSUMER_H_ + +#include <map> +#include <string> +#include <vector> + +class GoogleServiceAuthError; + +namespace net { +typedef std::vector<std::string> ResponseCookies; +} + +typedef std::map<std::string, std::string> UserInfoMap; + +// An interface that defines the callbacks for objects that +// GaiaAuthFetcher can return data to. +class GaiaAuthConsumer { + public: + struct ClientLoginResult { + ClientLoginResult(); + ClientLoginResult(const std::string& new_sid, + const std::string& new_lsid, + const std::string& new_token, + const std::string& new_data); + ~ClientLoginResult(); + + bool operator==(const ClientLoginResult &b) const; + + std::string sid; + std::string lsid; + std::string token; + // TODO(chron): Remove the data field later. Don't use it if possible. + std::string data; // Full contents of ClientLogin return. + bool two_factor; // set to true if there was a TWO_FACTOR "failure". + }; + + struct ClientOAuthResult { + ClientOAuthResult(); + ClientOAuthResult(const std::string& new_refresh_token, + const std::string& new_access_token, + int new_expires_in_secs); + ~ClientOAuthResult(); + + bool operator==(const ClientOAuthResult &b) const; + + // OAuth2 refresh token. Used to mint new access tokens when needed. + std::string refresh_token; + + // OAuth2 access token. Token to pass to endpoints that require oauth2 + // authentication. + std::string access_token; + + // The lifespan of |access_token| in seconds. + int expires_in_secs; + }; + + virtual ~GaiaAuthConsumer() {} + + virtual void OnClientLoginSuccess(const ClientLoginResult& result) {} + virtual void OnClientLoginFailure(const GoogleServiceAuthError& error) {} + + virtual void OnIssueAuthTokenSuccess(const std::string& service, + const std::string& auth_token) {} + virtual void OnIssueAuthTokenFailure(const std::string& service, + const GoogleServiceAuthError& error) {} + + virtual void OnClientOAuthSuccess(const ClientOAuthResult& result) {} + virtual void OnClientOAuthFailure(const GoogleServiceAuthError& error) {} + + virtual void OnGetUserInfoSuccess(const UserInfoMap& data) {} + virtual void OnGetUserInfoFailure(const GoogleServiceAuthError& error) {} + + virtual void OnUberAuthTokenSuccess(const std::string& token) {} + virtual void OnUberAuthTokenFailure(const GoogleServiceAuthError& error) {} + + virtual void OnMergeSessionSuccess(const std::string& data) {} + virtual void OnMergeSessionFailure(const GoogleServiceAuthError& error) {} +}; + +#endif // GOOGLE_APIS_GAIA_GAIA_AUTH_CONSUMER_H_ diff --git a/google_apis/gaia/gaia_auth_fetcher.cc b/google_apis/gaia/gaia_auth_fetcher.cc new file mode 100644 index 0000000..803ad6f --- /dev/null +++ b/google_apis/gaia/gaia_auth_fetcher.cc @@ -0,0 +1,1106 @@ +// 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 "google_apis/gaia/gaia_auth_fetcher.h" + +#include <algorithm> +#include <string> +#include <utility> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/string_split.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_auth_consumer.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 "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +namespace { +const int kLoadFlagsIgnoreCookies = net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES; + +static bool CookiePartsContains(const std::vector<std::string>& parts, + const char* part) { + return std::find(parts.begin(), parts.end(), part) != parts.end(); +} + +bool ExtractOAuth2TokenPairResponse(DictionaryValue* dict, + std::string* refresh_token, + std::string* access_token, + int* expires_in_secs) { + DCHECK(refresh_token); + DCHECK(access_token); + DCHECK(expires_in_secs); + + if (!dict->GetStringWithoutPathExpansion("refresh_token", refresh_token) || + !dict->GetStringWithoutPathExpansion("access_token", access_token) || + !dict->GetIntegerWithoutPathExpansion("expires_in", expires_in_secs)) { + return false; + } + + return true; +} + +} // namespace + +// TODO(chron): Add sourceless version of this formatter. +// static +const char GaiaAuthFetcher::kClientLoginFormat[] = + "Email=%s&" + "Passwd=%s&" + "PersistentCookie=%s&" + "accountType=%s&" + "source=%s&" + "service=%s"; +// static +const char GaiaAuthFetcher::kClientLoginCaptchaFormat[] = + "Email=%s&" + "Passwd=%s&" + "PersistentCookie=%s&" + "accountType=%s&" + "source=%s&" + "service=%s&" + "logintoken=%s&" + "logincaptcha=%s"; +// static +const char GaiaAuthFetcher::kIssueAuthTokenFormat[] = + "SID=%s&" + "LSID=%s&" + "service=%s&" + "Session=%s"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2BodyFormat[] = + "scope=%s&client_id=%s"; +// static +const char GaiaAuthFetcher::kOAuth2CodeToTokenPairBodyFormat[] = + "scope=%s&" + "grant_type=authorization_code&" + "client_id=%s&" + "client_secret=%s&" + "code=%s"; +// static +const char GaiaAuthFetcher::kGetUserInfoFormat[] = + "LSID=%s"; +// static +const char GaiaAuthFetcher::kMergeSessionFormat[] = + "uberauth=%s&" + "continue=%s&" + "source=%s"; +// static +const char GaiaAuthFetcher::kUberAuthTokenURLFormat[] = + "%s?source=%s&" + "issueuberauth=1"; + +const char GaiaAuthFetcher::kOAuthLoginFormat[] = "service=%s&source=%s"; + +// static +const char GaiaAuthFetcher::kAccountDeletedError[] = "AccountDeleted"; +const char GaiaAuthFetcher::kAccountDeletedErrorCode[] = "adel"; +// static +const char GaiaAuthFetcher::kAccountDisabledError[] = "AccountDisabled"; +const char GaiaAuthFetcher::kAccountDisabledErrorCode[] = "adis"; +// static +const char GaiaAuthFetcher::kBadAuthenticationError[] = "BadAuthentication"; +const char GaiaAuthFetcher::kBadAuthenticationErrorCode[] = "badauth"; +// static +const char GaiaAuthFetcher::kCaptchaError[] = "CaptchaRequired"; +const char GaiaAuthFetcher::kCaptchaErrorCode[] = "cr"; +// static +const char GaiaAuthFetcher::kServiceUnavailableError[] = + "ServiceUnavailable"; +const char GaiaAuthFetcher::kServiceUnavailableErrorCode[] = + "ire"; +// static +const char GaiaAuthFetcher::kErrorParam[] = "Error"; +// static +const char GaiaAuthFetcher::kErrorUrlParam[] = "Url"; +// static +const char GaiaAuthFetcher::kCaptchaUrlParam[] = "CaptchaUrl"; +// static +const char GaiaAuthFetcher::kCaptchaTokenParam[] = "CaptchaToken"; + +// static +const char GaiaAuthFetcher::kNeedsAdditional[] = "NeedsAdditional"; +// static +const char GaiaAuthFetcher::kCaptcha[] = "Captcha"; +// static +const char GaiaAuthFetcher::kTwoFactor[] = "TwoStep"; + +// static +const char GaiaAuthFetcher::kCookiePersistence[] = "true"; +// static +// TODO(johnnyg): When hosted accounts are supported by sync, +// we can always use "HOSTED_OR_GOOGLE" +const char GaiaAuthFetcher::kAccountTypeHostedOrGoogle[] = + "HOSTED_OR_GOOGLE"; +const char GaiaAuthFetcher::kAccountTypeGoogle[] = + "GOOGLE"; + +// static +const char GaiaAuthFetcher::kSecondFactor[] = "Info=InvalidSecondFactor"; + +// static +const char GaiaAuthFetcher::kAuthHeaderFormat[] = + "Authorization: GoogleLogin auth=%s"; +// static +const char GaiaAuthFetcher::kOAuthHeaderFormat[] = "Authorization: OAuth %s"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartSecure[] = "Secure"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartHttpOnly[] = + "HttpOnly"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartCodePrefix[] = + "oauth_code="; +// static +const int GaiaAuthFetcher::kClientLoginToOAuth2CookiePartCodePrefixLength = + arraysize(GaiaAuthFetcher::kClientLoginToOAuth2CookiePartCodePrefix) - 1; + +GaiaAuthFetcher::GaiaAuthFetcher(GaiaAuthConsumer* consumer, + const std::string& source, + net::URLRequestContextGetter* getter) + : consumer_(consumer), + getter_(getter), + source_(source), + client_login_gurl_(GaiaUrls::GetInstance()->client_login_url()), + issue_auth_token_gurl_(GaiaUrls::GetInstance()->issue_auth_token_url()), + oauth2_token_gurl_(GaiaUrls::GetInstance()->oauth2_token_url()), + get_user_info_gurl_(GaiaUrls::GetInstance()->get_user_info_url()), + merge_session_gurl_(GaiaUrls::GetInstance()->merge_session_url()), + uberauth_token_gurl_(base::StringPrintf(kUberAuthTokenURLFormat, + GaiaUrls::GetInstance()->oauth1_login_url().c_str(), source.c_str())), + client_oauth_gurl_(GaiaUrls::GetInstance()->client_oauth_url()), + oauth_login_gurl_(GaiaUrls::GetInstance()->oauth1_login_url()), + client_login_to_oauth2_gurl_( + GaiaUrls::GetInstance()->client_login_to_oauth2_url()), + fetch_pending_(false) {} + +GaiaAuthFetcher::~GaiaAuthFetcher() {} + +bool GaiaAuthFetcher::HasPendingFetch() { + return fetch_pending_; +} + +void GaiaAuthFetcher::CancelRequest() { + fetcher_.reset(); + fetch_pending_ = false; +} + +// static +net::URLFetcher* GaiaAuthFetcher::CreateGaiaFetcher( + net::URLRequestContextGetter* getter, + const std::string& body, + const std::string& headers, + const GURL& gaia_gurl, + int load_flags, + net::URLFetcherDelegate* delegate) { + net::URLFetcher* to_return = net::URLFetcher::Create( + 0, gaia_gurl, + body == "" ? net::URLFetcher::GET : net::URLFetcher::POST, + delegate); + to_return->SetRequestContext(getter); + to_return->SetUploadData("application/x-www-form-urlencoded", body); + + DVLOG(2) << "Gaia fetcher URL: " << gaia_gurl.spec(); + DVLOG(2) << "Gaia fetcher headers: " << headers; + DVLOG(2) << "Gaia fetcher body: " << body; + + // The Gaia token exchange requests do not require any cookie-based + // identification as part of requests. We suppress sending any cookies to + // maintain a separation between the user's browsing and Chrome's internal + // services. Where such mixing is desired (MergeSession), it will be done + // explicitly. + to_return->SetLoadFlags(load_flags); + + if (!headers.empty()) + to_return->SetExtraRequestHeaders(headers); + + return to_return; +} + +// static +std::string GaiaAuthFetcher::MakeClientLoginBody( + const std::string& username, + const std::string& password, + const std::string& source, + const char* service, + const std::string& login_token, + const std::string& login_captcha, + HostedAccountsSetting allow_hosted_accounts) { + std::string encoded_username = net::EscapeUrlEncodedData(username, true); + std::string encoded_password = net::EscapeUrlEncodedData(password, true); + std::string encoded_login_token = net::EscapeUrlEncodedData(login_token, + true); + std::string encoded_login_captcha = net::EscapeUrlEncodedData(login_captcha, + true); + + const char* account_type = allow_hosted_accounts == HostedAccountsAllowed ? + kAccountTypeHostedOrGoogle : + kAccountTypeGoogle; + + if (login_token.empty() || login_captcha.empty()) { + return base::StringPrintf(kClientLoginFormat, + encoded_username.c_str(), + encoded_password.c_str(), + kCookiePersistence, + account_type, + source.c_str(), + service); + } + + return base::StringPrintf(kClientLoginCaptchaFormat, + encoded_username.c_str(), + encoded_password.c_str(), + kCookiePersistence, + account_type, + source.c_str(), + service, + encoded_login_token.c_str(), + encoded_login_captcha.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeIssueAuthTokenBody( + const std::string& sid, + const std::string& lsid, + const char* const service) { + std::string encoded_sid = net::EscapeUrlEncodedData(sid, true); + std::string encoded_lsid = net::EscapeUrlEncodedData(lsid, true); + + // All tokens should be session tokens except the gaia auth token. + bool session = true; + if (!strcmp(service, GaiaConstants::kGaiaService)) + session = false; + + return base::StringPrintf(kIssueAuthTokenFormat, + encoded_sid.c_str(), + encoded_lsid.c_str(), + service, + session ? "true" : "false"); +} + +// static +std::string GaiaAuthFetcher::MakeGetAuthCodeBody() { + std::string encoded_scope = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth1_login_scope(), true); + std::string encoded_client_id = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth2_chrome_client_id(), true); + return StringPrintf(kClientLoginToOAuth2BodyFormat, + encoded_scope.c_str(), + encoded_client_id.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeGetTokenPairBody( + const std::string& auth_code) { + std::string encoded_scope = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth1_login_scope(), true); + std::string encoded_client_id = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth2_chrome_client_id(), true); + std::string encoded_client_secret = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), true); + std::string encoded_auth_code = net::EscapeUrlEncodedData(auth_code, true); + return StringPrintf(kOAuth2CodeToTokenPairBodyFormat, + encoded_scope.c_str(), + encoded_client_id.c_str(), + encoded_client_secret.c_str(), + encoded_auth_code.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeGetUserInfoBody(const std::string& lsid) { + std::string encoded_lsid = net::EscapeUrlEncodedData(lsid, true); + return base::StringPrintf(kGetUserInfoFormat, encoded_lsid.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeMergeSessionBody( + const std::string& auth_token, + const std::string& continue_url, + const std::string& source) { + std::string encoded_auth_token = net::EscapeUrlEncodedData(auth_token, true); + std::string encoded_continue_url = net::EscapeUrlEncodedData(continue_url, + true); + std::string encoded_source = net::EscapeUrlEncodedData(source, true); + return base::StringPrintf(kMergeSessionFormat, + encoded_auth_token.c_str(), + encoded_continue_url.c_str(), + encoded_source.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeGetAuthCodeHeader( + const std::string& auth_token) { + return StringPrintf(kAuthHeaderFormat, auth_token.c_str()); +} + +// Helper method that extracts tokens from a successful reply. +// static +void GaiaAuthFetcher::ParseClientLoginResponse(const std::string& data, + std::string* sid, + std::string* lsid, + std::string* token) { + using std::vector; + using std::pair; + using std::string; + + vector<pair<string, string> > tokens; + base::SplitStringIntoKeyValuePairs(data, '=', '\n', &tokens); + for (vector<pair<string, string> >::iterator i = tokens.begin(); + i != tokens.end(); ++i) { + if (i->first == "SID") { + sid->assign(i->second); + } else if (i->first == "LSID") { + lsid->assign(i->second); + } else if (i->first == "Auth") { + token->assign(i->second); + } + } +} + +// static +std::string GaiaAuthFetcher::MakeClientOAuthBody( + const std::string& username, + const std::string& password, + const std::vector<std::string>& scopes, + const std::string& persistent_id, + const std::string& friendly_name, + const std::string& locale) { + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + dict->SetString(GaiaConstants::kClientOAuthEmailKey, username); + dict->SetString(GaiaConstants::kClientOAuthPasswordKey, password); + + scoped_ptr<base::ListValue> scope_list(new base::ListValue); + for (size_t i = 0; i < scopes.size(); ++i) + scope_list->Append(base::Value::CreateStringValue(scopes[i])); + dict->Set(GaiaConstants::kClientOAuthScopesKey, scope_list.release()); + + dict->SetString(GaiaConstants::kClientOAuthOAuth2ClientIdKey, + GaiaUrls::GetInstance()->oauth2_chrome_client_id()); + // crbug.com/129600: use a less generic friendly name. + dict->SetString(GaiaConstants::kClientOAuthFriendlyDeviceNameKey, + friendly_name); + + scoped_ptr<base::ListValue> accepts_challenge_list(new base::ListValue); + accepts_challenge_list->Append(base::Value::CreateStringValue(kCaptcha)); + accepts_challenge_list->Append(base::Value::CreateStringValue(kTwoFactor)); + dict->Set(GaiaConstants::kClientOAuthAcceptsChallengesKey, + accepts_challenge_list.release()); + + dict->SetString(GaiaConstants::kClientOAuthLocaleKey, locale); + // Chrome presently does not not support a web-fallback for ClientOAuth, + // but need to hardcode an arbitrary one here since the endpoint expects it. + dict->SetString(GaiaConstants::kClientOAuthFallbackNameKey, "GetOAuth2Token"); + + std::string json_string; + base::JSONWriter::Write(dict.get(), &json_string); + return json_string; +} + +// static +std::string GaiaAuthFetcher::MakeClientOAuthChallengeResponseBody( + const std::string& name, + const std::string& token, + const std::string& solution) { + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + std::string field_name = name == kTwoFactor ? "otp" : "solution"; + + scoped_ptr<base::DictionaryValue> challenge_reply(new base::DictionaryValue); + challenge_reply->SetString(GaiaConstants::kClientOAuthNameKey, name); + challenge_reply->SetString(GaiaConstants::kClientOAuthChallengeTokenKey, + token); + challenge_reply->SetString(field_name, solution); + dict->Set(GaiaConstants::kClientOAuthchallengeReplyKey, + challenge_reply.release()); + + std::string json_string; + base::JSONWriter::Write(dict.get(), &json_string); + return json_string; +} + +// static +std::string GaiaAuthFetcher::MakeOAuthLoginBody(const std::string& service, + const std::string& source) { + std::string encoded_service = net::EscapeUrlEncodedData(service, true); + std::string encoded_source = net::EscapeUrlEncodedData(source, true); + return StringPrintf(kOAuthLoginFormat, encoded_service.c_str(), + encoded_source.c_str()); +} + +// static +void GaiaAuthFetcher::ParseClientLoginFailure(const std::string& data, + std::string* error, + std::string* error_url, + std::string* captcha_url, + std::string* captcha_token) { + using std::vector; + using std::pair; + using std::string; + + vector<pair<string, string> > tokens; + base::SplitStringIntoKeyValuePairs(data, '=', '\n', &tokens); + for (vector<pair<string, string> >::iterator i = tokens.begin(); + i != tokens.end(); ++i) { + if (i->first == kErrorParam) { + error->assign(i->second); + } else if (i->first == kErrorUrlParam) { + error_url->assign(i->second); + } else if (i->first == kCaptchaUrlParam) { + captcha_url->assign(i->second); + } else if (i->first == kCaptchaTokenParam) { + captcha_token->assign(i->second); + } + } +} + +// static +bool GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + const net::ResponseCookies& cookies, + std::string* auth_code) { + DCHECK(auth_code); + net::ResponseCookies::const_iterator iter; + for (iter = cookies.begin(); iter != cookies.end(); ++iter) { + if (ParseClientLoginToOAuth2Cookie(*iter, auth_code)) + return true; + } + return false; +} + +// static +bool GaiaAuthFetcher::ParseClientLoginToOAuth2Cookie(const std::string& cookie, + std::string* auth_code) { + std::vector<std::string> parts; + base::SplitString(cookie, ';', &parts); + // Per documentation, the cookie should have Secure and HttpOnly. + if (!CookiePartsContains(parts, kClientLoginToOAuth2CookiePartSecure) || + !CookiePartsContains(parts, kClientLoginToOAuth2CookiePartHttpOnly)) { + return false; + } + + std::vector<std::string>::const_iterator iter; + for (iter = parts.begin(); iter != parts.end(); ++iter) { + const std::string& part = *iter; + if (StartsWithASCII( + part, kClientLoginToOAuth2CookiePartCodePrefix, false)) { + auth_code->assign(part.substr( + kClientLoginToOAuth2CookiePartCodePrefixLength)); + return true; + } + } + return false; +} + +// static +GoogleServiceAuthError +GaiaAuthFetcher::GenerateClientOAuthError(const std::string& data, + const net::URLRequestStatus& status) { + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + return GenerateAuthError(data, status); + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + + std::string cause; + if (!dict->GetStringWithoutPathExpansion("cause", &cause)) + return GoogleServiceAuthError::FromClientOAuthError(data); + + if (cause != kNeedsAdditional) + return GoogleServiceAuthError::FromClientOAuthError(data); + + DictionaryValue* challenge; + if (!dict->GetDictionaryWithoutPathExpansion("challenge", &challenge)) + return GoogleServiceAuthError::FromClientOAuthError(data); + + std::string name; + if (!challenge->GetStringWithoutPathExpansion("name", &name)) + return GoogleServiceAuthError::FromClientOAuthError(data); + + if (name == kCaptcha) { + std::string token; + std::string audio_url; + std::string image_url; + int image_width; + int image_height; + if (!challenge->GetStringWithoutPathExpansion("challenge_token", &token) || + !challenge->GetStringWithoutPathExpansion("audio_url", &audio_url) || + !challenge->GetStringWithoutPathExpansion("image_url", &image_url) || + !challenge->GetIntegerWithoutPathExpansion("image_width", + &image_width) || + !challenge->GetIntegerWithoutPathExpansion("image_height", + &image_height)) { + return GoogleServiceAuthError::FromClientOAuthError(data); + } + return GoogleServiceAuthError::FromCaptchaChallenge(token, GURL(audio_url), + GURL(image_url), + image_width, + image_height); + } else if (name == kTwoFactor) { + std::string token; + std::string prompt_text; + std::string alternate_text; + int field_length; + + // The protocol doc says these are required, but in practice they are not + // returned. So only a missing challenge token will cause an error here. + challenge->GetStringWithoutPathExpansion("prompt_text", &prompt_text); + challenge->GetStringWithoutPathExpansion("alternate_text", &alternate_text); + challenge->GetIntegerWithoutPathExpansion("field_length", &field_length); + if (!challenge->GetStringWithoutPathExpansion("challenge_token", &token)) + return GoogleServiceAuthError::FromClientOAuthError(data); + + return GoogleServiceAuthError::FromSecondFactorChallenge(token, prompt_text, + alternate_text, + field_length); + } + + return GoogleServiceAuthError::FromClientOAuthError(data); +} + +void GaiaAuthFetcher::StartClientLogin( + const std::string& username, + const std::string& password, + const char* const service, + const std::string& login_token, + const std::string& login_captcha, + HostedAccountsSetting allow_hosted_accounts) { + + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + // This class is thread agnostic, so be sure to call this only on the + // same thread each time. + DVLOG(1) << "Starting new ClientLogin fetch for:" << username; + + // Must outlive fetcher_. + request_body_ = MakeClientLoginBody(username, + password, + source_, + service, + login_token, + login_captcha, + allow_hosted_accounts); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + "", + client_login_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartIssueAuthToken(const std::string& sid, + const std::string& lsid, + const char* const service) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting IssueAuthToken for: " << service; + requested_service_ = service; + request_body_ = MakeIssueAuthTokenBody(sid, lsid, service); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + "", + issue_auth_token_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartLsoForOAuthLoginTokenExchange( + const std::string& auth_token) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting OAuth login token exchange with auth_token"; + request_body_ = MakeGetAuthCodeBody(); + client_login_to_oauth2_gurl_ = + GURL(GaiaUrls::GetInstance()->client_login_to_oauth2_url()); + + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + MakeGetAuthCodeHeader(auth_token), + client_login_to_oauth2_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartCookieForOAuthLoginTokenExchange( + const std::string& session_index) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting OAuth login token fetch with cookie jar"; + request_body_ = MakeGetAuthCodeBody(); + + std::string url = GaiaUrls::GetInstance()->client_login_to_oauth2_url(); + if (!session_index.empty()) + url += "?authuser=" + session_index; + + client_login_to_oauth2_gurl_ = GURL(url); + + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + "", + client_login_to_oauth2_gurl_, + net::LOAD_NORMAL, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartGetUserInfo(const std::string& lsid) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting GetUserInfo for lsid=" << lsid; + request_body_ = MakeGetUserInfoBody(lsid); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + "", + get_user_info_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartMergeSession(const std::string& uber_token) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting MergeSession with uber_token=" << uber_token; + + // The continue URL is a required parameter of the MergeSession API, but in + // this case we don't actually need or want to navigate to it. Setting it to + // an arbitrary Google URL. + // + // In order for the new session to be merged correctly, the server needs to + // know what sessions already exist in the browser. The fetcher needs to be + // created such that it sends the cookies with the request, which is + // different from all other requests the fetcher can make. + std::string continue_url("http://www.google.com"); + request_body_ = MakeMergeSessionBody(uber_token, continue_url, source_); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + "", + merge_session_gurl_, + net::LOAD_NORMAL, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartTokenFetchForUberAuthExchange( + const std::string& access_token) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting StartTokenFetchForUberAuthExchange with access_token=" + << access_token; + std::string authentication_header = + base::StringPrintf(kOAuthHeaderFormat, access_token.c_str()); + fetcher_.reset(CreateGaiaFetcher(getter_, + "", + authentication_header, + uberauth_token_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartClientOAuth(const std::string& username, + const std::string& password, + const std::vector<std::string>& scopes, + const std::string& persistent_id, + const std::string& locale) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + request_body_ = MakeClientOAuthBody(username, password, scopes, persistent_id, + source_, locale); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + "", + client_oauth_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartClientOAuthChallengeResponse( + GoogleServiceAuthError::State type, + const std::string& token, + const std::string& solution) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + std::string name; + switch (type) { + case GoogleServiceAuthError::CAPTCHA_REQUIRED: + name = kCaptcha; + break; + case GoogleServiceAuthError::TWO_FACTOR: + name = kTwoFactor; + break; + default: + NOTREACHED(); + } + + request_body_ = MakeClientOAuthChallengeResponseBody(name, token, solution); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + "", + client_oauth_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartOAuthLogin(const std::string& access_token, + const std::string& service) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + request_body_ = MakeOAuthLoginBody(service, source_); + std::string authentication_header = + base::StringPrintf("Authorization: Bearer %s", access_token.c_str()); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + authentication_header, + oauth_login_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +// static +GoogleServiceAuthError GaiaAuthFetcher::GenerateAuthError( + const std::string& data, + const net::URLRequestStatus& status) { + if (!status.is_success()) { + if (status.status() == net::URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } + } else { + if (IsSecondFactorSuccess(data)) { + return GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); + } + + std::string error; + std::string url; + std::string captcha_url; + std::string captcha_token; + ParseClientLoginFailure(data, &error, &url, &captcha_url, &captcha_token); + DLOG(WARNING) << "ClientLogin failed with " << error; + + if (error == kCaptchaError) { + GURL image_url( + GaiaUrls::GetInstance()->captcha_url_prefix() + captcha_url); + GURL unlock_url(url); + return GoogleServiceAuthError::FromClientLoginCaptchaChallenge( + captcha_token, image_url, unlock_url); + } + if (error == kAccountDeletedError) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DELETED); + if (error == kAccountDisabledError) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DISABLED); + if (error == kBadAuthenticationError) { + return GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + } + if (error == kServiceUnavailableError) { + return GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE); + } + + DLOG(WARNING) << "Incomprehensible response from Google Accounts servers."; + return GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE); + } + + NOTREACHED(); + return GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +// static +GoogleServiceAuthError GaiaAuthFetcher::GenerateOAuthLoginError( + const std::string& data, + const net::URLRequestStatus& status) { + if (!status.is_success()) { + if (status.status() == net::URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } + } else { + if (IsSecondFactorSuccess(data)) { + return GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); + } + + std::string error; + std::string url; + std::string captcha_url; + std::string captcha_token; + ParseClientLoginFailure(data, &error, &url, &captcha_url, &captcha_token); + LOG(WARNING) << "OAuthLogin failed with " << error; + + if (error == kCaptchaErrorCode) { + GURL image_url( + GaiaUrls::GetInstance()->captcha_url_prefix() + captcha_url); + GURL unlock_url(url); + return GoogleServiceAuthError::FromClientLoginCaptchaChallenge( + captcha_token, image_url, unlock_url); + } + if (error == kAccountDeletedErrorCode) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DELETED); + if (error == kAccountDisabledErrorCode) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DISABLED); + if (error == kBadAuthenticationErrorCode) { + return GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + } + if (error == kServiceUnavailableErrorCode) { + return GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE); + } + + DLOG(WARNING) << "Incomprehensible response from Google Accounts servers."; + return GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE); + } + + NOTREACHED(); + return GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +void GaiaAuthFetcher::OnClientLoginFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + DVLOG(1) << "ClientLogin successful!"; + std::string sid; + std::string lsid; + std::string token; + ParseClientLoginResponse(data, &sid, &lsid, &token); + consumer_->OnClientLoginSuccess( + GaiaAuthConsumer::ClientLoginResult(sid, lsid, token, data)); + } else { + consumer_->OnClientLoginFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnIssueAuthTokenFetched( + const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + // Only the bare token is returned in the body of this Gaia call + // without any padding. + consumer_->OnIssueAuthTokenSuccess(requested_service_, data); + } else { + consumer_->OnIssueAuthTokenFailure(requested_service_, + GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnClientLoginToOAuth2Fetched( + const std::string& data, + const net::ResponseCookies& cookies, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + std::string auth_code; + ParseClientLoginToOAuth2Response(cookies, &auth_code); + StartOAuth2TokenPairFetch(auth_code); + } else { + consumer_->OnClientOAuthFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::StartOAuth2TokenPairFetch(const std::string& auth_code) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting OAuth token pair fetch"; + request_body_ = MakeGetTokenPairBody(auth_code); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + "", + oauth2_token_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::OnOAuth2TokenPairFetched( + const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + std::string refresh_token; + std::string access_token; + int expires_in_secs = 0; + + bool success = false; + if (status.is_success() && response_code == net::HTTP_OK) { + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (value.get() && value->GetType() == base::Value::TYPE_DICTIONARY) { + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + success = ExtractOAuth2TokenPairResponse(dict, &refresh_token, + &access_token, &expires_in_secs); + } + } + + if (success) { + consumer_->OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult(refresh_token, access_token, + expires_in_secs)); + } else { + consumer_->OnClientOAuthFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnGetUserInfoFetched( + const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + std::vector<std::pair<std::string, std::string> > tokens; + UserInfoMap matches; + base::SplitStringIntoKeyValuePairs(data, '=', '\n', &tokens); + std::vector<std::pair<std::string, std::string> >::iterator i; + for (i = tokens.begin(); i != tokens.end(); ++i) { + matches[i->first] = i->second; + } + consumer_->OnGetUserInfoSuccess(matches); + } else { + consumer_->OnGetUserInfoFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnMergeSessionFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + consumer_->OnMergeSessionSuccess(data); + } else { + consumer_->OnMergeSessionFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnUberAuthTokenFetch(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + consumer_->OnUberAuthTokenSuccess(data); + } else { + consumer_->OnUberAuthTokenFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnClientOAuthFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + std::string refresh_token; + std::string access_token; + int expires_in_secs = 0; + + bool success = false; + if (status.is_success() && response_code == net::HTTP_OK) { + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (value.get() && value->GetType() == base::Value::TYPE_DICTIONARY) { + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + DictionaryValue* dict_oauth2; + if (dict->GetDictionaryWithoutPathExpansion("oauth2", &dict_oauth2)) { + success = ExtractOAuth2TokenPairResponse(dict_oauth2, &refresh_token, + &access_token, + &expires_in_secs); + } + } + } + + // TODO(rogerta): for now this reuses the OnOAuthLoginTokenXXX callbacks + // since the data is exactly the same. This ignores the optional + // persistent_id data in the response, which we may need to handle. + // If we do, we'll need to modify ExtractOAuth2TokenPairResponse() to parse + // the optional data and declare new consumer callbacks to take it. + if (success) { + consumer_->OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult(refresh_token, access_token, + expires_in_secs)); + } else { + consumer_->OnClientOAuthFailure(GenerateClientOAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnOAuthLoginFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + DVLOG(1) << "ClientLogin successful!"; + std::string sid; + std::string lsid; + std::string token; + ParseClientLoginResponse(data, &sid, &lsid, &token); + consumer_->OnClientLoginSuccess( + GaiaAuthConsumer::ClientLoginResult(sid, lsid, token, data)); + } else { + consumer_->OnClientLoginFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnURLFetchComplete(const net::URLFetcher* source) { + fetch_pending_ = false; + // Some of the GAIA requests perform redirects, which results in the final + // URL of the fetcher not being the original URL requested. Therefore use + // the original URL when determining which OnXXX function to call. + const GURL& url = source->GetOriginalURL(); + const net::URLRequestStatus& status = source->GetStatus(); + int response_code = source->GetResponseCode(); + std::string data; + source->GetResponseAsString(&data); + DVLOG(2) << "Gaia fetcher response code: " << response_code; + DVLOG(2) << "Gaia fetcher response data: " << data; + if (url == client_login_gurl_) { + OnClientLoginFetched(data, status, response_code); + } else if (url == issue_auth_token_gurl_) { + OnIssueAuthTokenFetched(data, status, response_code); + } else if (url == client_login_to_oauth2_gurl_) { + OnClientLoginToOAuth2Fetched( + data, source->GetCookies(), status, response_code); + } else if (url == oauth2_token_gurl_) { + OnOAuth2TokenPairFetched(data, status, response_code); + } else if (url == get_user_info_gurl_) { + OnGetUserInfoFetched(data, status, response_code); + } else if (url == merge_session_gurl_) { + OnMergeSessionFetched(data, status, response_code); + } else if (url == uberauth_token_gurl_) { + OnUberAuthTokenFetch(data, status, response_code); + } else if (url == client_oauth_gurl_) { + OnClientOAuthFetched(data, status, response_code); + } else if (url == oauth_login_gurl_) { + OnOAuthLoginFetched(data, status, response_code); + } else { + NOTREACHED(); + } +} + +// static +bool GaiaAuthFetcher::IsSecondFactorSuccess( + const std::string& alleged_error) { + return alleged_error.find(kSecondFactor) != + std::string::npos; +} diff --git a/google_apis/gaia/gaia_auth_fetcher.h b/google_apis/gaia/gaia_auth_fetcher.h new file mode 100644 index 0000000..2aea05b --- /dev/null +++ b/google_apis/gaia/gaia_auth_fetcher.h @@ -0,0 +1,406 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_GAIA_AUTH_FETCHER_H_ +#define GOOGLE_APIS_GAIA_GAIA_AUTH_FETCHER_H_ + +#include <string> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/gaia_auth_consumer.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "googleurl/src/gurl.h" +#include "net/url_request/url_fetcher_delegate.h" + +// Authenticate a user against the Google Accounts ClientLogin API +// with various capabilities and return results to a GaiaAuthConsumer. +// +// In the future, we will also issue auth tokens from this class. +// This class should be used on a single thread, but it can be whichever thread +// that you like. +// +// This class can handle one request at a time on any thread. To parallelize +// requests, create multiple GaiaAuthFetcher's. + +class GaiaAuthFetcherTest; + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +class URLRequestStatus; +} + +class GaiaAuthFetcher : public net::URLFetcherDelegate { + public: + enum HostedAccountsSetting { + HostedAccountsAllowed, + HostedAccountsNotAllowed + }; + + // Magic string indicating that, while a second factor is still + // needed to complete authentication, the user provided the right password. + static const char kSecondFactor[]; + + // This will later be hidden behind an auth service which caches + // tokens. + GaiaAuthFetcher(GaiaAuthConsumer* consumer, + const std::string& source, + net::URLRequestContextGetter* getter); + virtual ~GaiaAuthFetcher(); + + // Start a request to obtain the SID and LSID cookies for the the account + // identified by |username| and |password|. If |service| is not null or + // empty, then also obtains a service token for specified service. + // + // If this is a second call because of captcha challenge, then the + // |login_token| and |login_captcha| arugment should correspond to the + // solution of the challenge. + // + // Either OnClientLoginSuccess or OnClientLoginFailure will be + // called on the consumer on the original thread. + void StartClientLogin(const std::string& username, + const std::string& password, + const char* const service, + const std::string& login_token, + const std::string& login_captcha, + HostedAccountsSetting allow_hosted_accounts); + + // Start a request to obtain service token for the the account identified by + // |sid| and |lsid| and the service|service|. + // + // Either OnIssueAuthTokenSuccess or OnIssueAuthTokenFailure will be + // called on the consumer on the original thread. + void StartIssueAuthToken(const std::string& sid, + const std::string& lsid, + const char* const service); + + // Start a request to exchange an "lso" service token given by |auth_token| + // for an OAuthLogin-scoped oauth2 token. + // + // Either OnClientOAuthSuccess or OnClientOAuthFailure will be + // called on the consumer on the original thread. + void StartLsoForOAuthLoginTokenExchange(const std::string& auth_token); + + // Start a request to exchange the cookies of a signed-in user session + // for an OAuthLogin-scoped oauth2 token. In the case of a session with + // multiple accounts signed in, |session_index| indicate the which of accounts + // within the session. + // + // Either OnClientOAuthSuccess or OnClientOAuthFailure will be + // called on the consumer on the original thread. + void StartCookieForOAuthLoginTokenExchange(const std::string& session_index); + + // Start a request to get user info for the account identified by |lsid|. + // + // Either OnGetUserInfoSuccess or OnGetUserInfoFailure will be + // called on the consumer on the original thread. + void StartGetUserInfo(const std::string& lsid); + + // Start a MergeSession request to pre-login the user with the given + // credentials. + // + // Start a MergeSession request to fill the browsing cookie jar with + // credentials represented by the account whose uber-auth token is + // |uber_token|. This method will modify the cookies of the current profile. + // + // Either OnMergeSessionSuccess or OnMergeSessionFailure will be + // called on the consumer on the original thread. + void StartMergeSession(const std::string& uber_token); + + // Start a request to exchange an OAuthLogin-scoped oauth2 access token for an + // uber-auth token. The returned token can be used with the method + // StartMergeSession(). + // + // Either OnUberAuthTokenSuccess or OnUberAuthTokenFailure will be + // called on the consumer on the original thread. + void StartTokenFetchForUberAuthExchange(const std::string& access_token); + + // Start a request to obtain an OAuth2 token for the account identified by + // |username| and |password|. |scopes| is a list of oauth scopes that + // indicate the access permerssions to assign to the returned token. + // |persistent_id| is an optional client identifier used to identify this + // particular chrome instances, which may reduce the chance of a challenge. + // |locale| will be used to format messages to be presented to the user in + // challenges, if needed. + // + // If the request cannot complete due to a challenge, the + // GoogleServiceAuthError will indicate the type of challenge required: + // either CAPTCHA_REQUIRED or TWO_FACTOR. + // + // Either OnClientOAuthSuccess or OnClientOAuthFailure will be + // called on the consumer on the original thread. + void StartClientOAuth(const std::string& username, + const std::string& password, + const std::vector<std::string>& scopes, + const std::string& persistent_id, + const std::string& locale); + + // Start a challenge response to obtain an OAuth2 token. This method is + // called after a challenge response is issued from a previous call to + // StartClientOAuth(). The |type| and |token| arguments come from the + // error response to StartClientOAuth(), while the |solution| argument + // represents the answer from the user for the partocular challenge. + // + // Either OnClientOAuthSuccess or OnClientOAuthFailure will be + // called on the consumer on the original thread. + void StartClientOAuthChallengeResponse(GoogleServiceAuthError::State type, + const std::string& token, + const std::string& solution); + + // Start a request to exchange an OAuthLogin-scoped oauth2 access token for a + // ClientLogin-style service tokens. The response to this request is the + // same as the response to a ClientLogin request, except that captcha + // challenges are never issued. + // + // Either OnClientLoginSuccess or OnClientLoginFailure will be + // called on the consumer on the original thread. + void StartOAuthLogin(const std::string& access_token, + const std::string& service); + + // Implementation of net::URLFetcherDelegate + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + // StartClientLogin been called && results not back yet? + bool HasPendingFetch(); + + // Stop any URL fetches in progress. + void CancelRequest(); + + // From a URLFetcher result, generate an appropriate error. + // From the API documentation, both IssueAuthToken and ClientLogin have + // the same error returns. + static GoogleServiceAuthError GenerateOAuthLoginError( + const std::string& data, + const net::URLRequestStatus& status); + + private: + // ClientLogin body constants that don't change + static const char kCookiePersistence[]; + static const char kAccountTypeHostedOrGoogle[]; + static const char kAccountTypeGoogle[]; + + // The format of the POST body for ClientLogin. + static const char kClientLoginFormat[]; + // The format of said POST body when CAPTCHA token & answer are specified. + static const char kClientLoginCaptchaFormat[]; + // The format of the POST body for IssueAuthToken. + static const char kIssueAuthTokenFormat[]; + // The format of the POST body to get OAuth2 auth code from auth token. + static const char kClientLoginToOAuth2BodyFormat[]; + // The format of the POST body to get OAuth2 token pair from auth code. + static const char kOAuth2CodeToTokenPairBodyFormat[]; + // The format of the POST body for GetUserInfo. + static const char kGetUserInfoFormat[]; + // The format of the POST body for MergeSession. + static const char kMergeSessionFormat[]; + // The format of the URL for UberAuthToken. + static const char kUberAuthTokenURLFormat[]; + // The format of the body for OAuthLogin. + static const char kOAuthLoginFormat[]; + + // Constants for parsing ClientLogin errors. + static const char kAccountDeletedError[]; + static const char kAccountDeletedErrorCode[]; + static const char kAccountDisabledError[]; + static const char kAccountDisabledErrorCode[]; + static const char kBadAuthenticationError[]; + static const char kBadAuthenticationErrorCode[]; + static const char kCaptchaError[]; + static const char kCaptchaErrorCode[]; + static const char kServiceUnavailableError[]; + static const char kServiceUnavailableErrorCode[]; + static const char kErrorParam[]; + static const char kErrorUrlParam[]; + static const char kCaptchaUrlParam[]; + static const char kCaptchaTokenParam[]; + + // Constants for parsing ClientOAuth errors. + static const char kNeedsAdditional[]; + static const char kCaptcha[]; + static const char kTwoFactor[]; + + // Constants for request/response for OAuth2 requests. + static const char kAuthHeaderFormat[]; + static const char kOAuthHeaderFormat[]; + static const char kClientLoginToOAuth2CookiePartSecure[]; + static const char kClientLoginToOAuth2CookiePartHttpOnly[]; + static const char kClientLoginToOAuth2CookiePartCodePrefix[]; + static const int kClientLoginToOAuth2CookiePartCodePrefixLength; + + // Process the results of a ClientLogin fetch. + void OnClientLoginFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnIssueAuthTokenFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnClientLoginToOAuth2Fetched(const std::string& data, + const net::ResponseCookies& cookies, + const net::URLRequestStatus& status, + int response_code); + + void OnOAuth2TokenPairFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnGetUserInfoFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnMergeSessionFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnUberAuthTokenFetch(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnClientOAuthFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnOAuthLoginFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + // Tokenize the results of a ClientLogin fetch. + static void ParseClientLoginResponse(const std::string& data, + std::string* sid, + std::string* lsid, + std::string* token); + + static void ParseClientLoginFailure(const std::string& data, + std::string* error, + std::string* error_url, + std::string* captcha_url, + std::string* captcha_token); + + // Parse ClientLogin to OAuth2 response. + static bool ParseClientLoginToOAuth2Response( + const net::ResponseCookies& cookies, + std::string* auth_code); + + static bool ParseClientLoginToOAuth2Cookie(const std::string& cookie, + std::string* auth_code); + + static GoogleServiceAuthError GenerateClientOAuthError( + const std::string& data, + const net::URLRequestStatus& status); + + // Is this a special case Gaia error for TwoFactor auth? + static bool IsSecondFactorSuccess(const std::string& alleged_error); + + // Given parameters, create a ClientLogin request body. + static std::string MakeClientLoginBody( + const std::string& username, + const std::string& password, + const std::string& source, + const char* const service, + const std::string& login_token, + const std::string& login_captcha, + HostedAccountsSetting allow_hosted_accounts); + // Supply the sid / lsid returned from ClientLogin in order to + // request a long lived auth token for a service. + static std::string MakeIssueAuthTokenBody(const std::string& sid, + const std::string& lsid, + const char* const service); + // Create body to get OAuth2 auth code. + static std::string MakeGetAuthCodeBody(); + // Given auth code, create body to get OAuth2 token pair. + static std::string MakeGetTokenPairBody(const std::string& auth_code); + // Supply the lsid returned from ClientLogin in order to fetch + // user information. + static std::string MakeGetUserInfoBody(const std::string& lsid); + + // Supply the authentication token returned from StartIssueAuthToken. + static std::string MakeMergeSessionBody(const std::string& auth_token, + const std::string& continue_url, + const std::string& source); + + static std::string MakeGetAuthCodeHeader(const std::string& auth_token); + + static std::string MakeClientOAuthBody(const std::string& username, + const std::string& password, + const std::vector<std::string>& scopes, + const std::string& persistent_id, + const std::string& friendly_name, + const std::string& locale); + + static std::string MakeClientOAuthChallengeResponseBody( + const std::string& name, + const std::string& token, + const std::string& solution); + + static std::string MakeOAuthLoginBody(const std::string& service, + const std::string& source); + + void StartOAuth2TokenPairFetch(const std::string& auth_code); + + // Create a fetcher usable for making any Gaia request. |body| is used + // as the body of the POST request sent to GAIA. Any strings listed in + // |headers| are added as extra HTTP headers in the request. + // + // |load_flags| are passed to directly to net::URLFetcher::Create() when + // creating the URL fetcher. + static net::URLFetcher* CreateGaiaFetcher( + net::URLRequestContextGetter* getter, + const std::string& body, + const std::string& headers, + const GURL& gaia_gurl, + int load_flags, + net::URLFetcherDelegate* delegate); + + // From a URLFetcher result, generate an appropriate error. + // From the API documentation, both IssueAuthToken and ClientLogin have + // the same error returns. + static GoogleServiceAuthError GenerateAuthError( + const std::string& data, + const net::URLRequestStatus& status); + + // These fields are common to GaiaAuthFetcher, same every request + GaiaAuthConsumer* const consumer_; + net::URLRequestContextGetter* const getter_; + std::string source_; + const GURL client_login_gurl_; + const GURL issue_auth_token_gurl_; + const GURL oauth2_token_gurl_; + const GURL get_user_info_gurl_; + const GURL merge_session_gurl_; + const GURL uberauth_token_gurl_; + const GURL client_oauth_gurl_; + const GURL oauth_login_gurl_; + + // While a fetch is going on: + scoped_ptr<net::URLFetcher> fetcher_; + GURL client_login_to_oauth2_gurl_; + std::string request_body_; + std::string requested_service_; // Currently tracked for IssueAuthToken only. + bool fetch_pending_; + + friend class GaiaAuthFetcherTest; + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, CaptchaParse); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, AccountDeletedError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, AccountDisabledError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, BadAuthenticationError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, IncomprehensibleError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ServiceUnavailableError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, CheckNormalErrorCode); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, CheckTwoFactorResponse); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, LoginNetFailure); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, + ParseClientLoginToOAuth2Response); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ParseOAuth2TokenPairResponse); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ClientOAuthSuccess); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ClientOAuthWithQuote); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ClientOAuthChallengeSuccess); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ClientOAuthChallengeQuote); + + DISALLOW_COPY_AND_ASSIGN(GaiaAuthFetcher); +}; + +#endif // GOOGLE_APIS_GAIA_GAIA_AUTH_FETCHER_H_ diff --git a/google_apis/gaia/gaia_auth_fetcher_unittest.cc b/google_apis/gaia/gaia_auth_fetcher_unittest.cc new file mode 100644 index 0000000..cfa617d --- /dev/null +++ b/google_apis/gaia/gaia_auth_fetcher_unittest.cc @@ -0,0 +1,1021 @@ +// 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. +// +// A complete set of unit tests for GaiaAuthFetcher. +// Originally ported from GoogleAuthenticator tests. + +#include <string> + +#include "base/json/json_reader.h" +#include "base/message_loop.h" +#include "base/stringprintf.h" +#include "base/values.h" +#include "chrome/test/base/testing_profile.h" +#include "google_apis/gaia/gaia_auth_consumer.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/mock_url_fetcher_factory.h" +#include "googleurl/src/gurl.h" +#include "net/base/load_flags.h" +#include "net/base/net_errors.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_status.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using ::testing::Invoke; + +namespace { +static const char kGetAuthCodeValidCookie[] = + "oauth_code=test-code; Path=/test; Secure; HttpOnly"; +static const char kGetAuthCodeCookieNoSecure[] = + "oauth_code=test-code; Path=/test; HttpOnly"; +static const char kGetAuthCodeCookieNoHttpOnly[] = + "oauth_code=test-code; Path=/test; Secure"; +static const char kGetAuthCodeCookieNoOAuthCode[] = + "Path=/test; Secure; HttpOnly"; +static const char kGetTokenPairValidResponse[] = + "{" + " \"refresh_token\": \"rt1\"," + " \"access_token\": \"at1\"," + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + "}"; +static const char kClientOAuthValidResponse[] = + "{" + " \"oauth2\": {" + " \"refresh_token\": \"rt1\"," + " \"access_token\": \"at1\"," + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + " }" + "}"; + +static void ExpectCaptchaChallenge(const GoogleServiceAuthError& error) { + // Make sure this is a captcha server challange. + EXPECT_EQ(GoogleServiceAuthError::CAPTCHA_REQUIRED, error.state()); + EXPECT_EQ("challengetokenblob", error.captcha().token); + EXPECT_EQ("http://www.audio.com/", error.captcha().audio_url.spec()); + EXPECT_EQ("http://www.image.com/", error.captcha().image_url.spec()); + EXPECT_EQ(640, error.captcha().image_width); + EXPECT_EQ(480, error.captcha().image_height); +} + +static void ExpectBadAuth(const GoogleServiceAuthError& error) { + EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error.state()); +} + +static void ExpectTwoFactorChallenge(const GoogleServiceAuthError& error) { + // Make sure this is a captcha server challange. + EXPECT_EQ(GoogleServiceAuthError::TWO_FACTOR, error.state()); + EXPECT_EQ("challengetokenblob", error.second_factor().token); + EXPECT_EQ("prompt_text", error.second_factor().prompt_text); + EXPECT_EQ("alternate_text", error.second_factor().alternate_text); + EXPECT_EQ(10, error.second_factor().field_length); +} + +} // namespace + +MockFetcher::MockFetcher(bool success, + const GURL& url, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) + : TestURLFetcher(0, url, d) { + set_url(url); + net::URLRequestStatus::Status code; + + if (success) { + set_response_code(net::HTTP_OK); + code = net::URLRequestStatus::SUCCESS; + } else { + set_response_code(net::HTTP_FORBIDDEN); + code = net::URLRequestStatus::FAILED; + } + + set_status(net::URLRequestStatus(code, 0)); + SetResponseString(results); +} + +MockFetcher::MockFetcher(const GURL& url, + const net::URLRequestStatus& status, + int response_code, + const net::ResponseCookies& cookies, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) + : TestURLFetcher(0, url, d) { + set_url(url); + set_status(status); + set_response_code(response_code); + set_cookies(cookies); + SetResponseString(results); +} + +MockFetcher::~MockFetcher() {} + +void MockFetcher::Start() { + delegate()->OnURLFetchComplete(this); +} + +class GaiaAuthFetcherTest : public testing::Test { + public: + GaiaAuthFetcherTest() + : client_login_source_(GaiaUrls::GetInstance()->client_login_url()), + issue_auth_token_source_( + GaiaUrls::GetInstance()->issue_auth_token_url()), + client_login_to_oauth2_source_( + GaiaUrls::GetInstance()->client_login_to_oauth2_url()), + oauth2_token_source_(GaiaUrls::GetInstance()->oauth2_token_url()), + token_auth_source_(GaiaUrls::GetInstance()->token_auth_url()), + merge_session_source_(GaiaUrls::GetInstance()->merge_session_url()), + uberauth_token_source_(base::StringPrintf( + "%s?source=&issueuberauth=1", + GaiaUrls::GetInstance()->oauth1_login_url().c_str())), + client_oauth_source_(GaiaUrls::GetInstance()->client_oauth_url()), + oauth_login_gurl_(GaiaUrls::GetInstance()->oauth1_login_url()) {} + + void RunParsingTest(const std::string& data, + const std::string& sid, + const std::string& lsid, + const std::string& token) { + std::string out_sid; + std::string out_lsid; + std::string out_token; + + GaiaAuthFetcher::ParseClientLoginResponse(data, + &out_sid, + &out_lsid, + &out_token); + EXPECT_EQ(lsid, out_lsid); + EXPECT_EQ(sid, out_sid); + EXPECT_EQ(token, out_token); + } + + void RunErrorParsingTest(const std::string& data, + const std::string& error, + const std::string& error_url, + const std::string& captcha_url, + const std::string& captcha_token) { + std::string out_error; + std::string out_error_url; + std::string out_captcha_url; + std::string out_captcha_token; + + GaiaAuthFetcher::ParseClientLoginFailure(data, + &out_error, + &out_error_url, + &out_captcha_url, + &out_captcha_token); + EXPECT_EQ(error, out_error); + EXPECT_EQ(error_url, out_error_url); + EXPECT_EQ(captcha_url, out_captcha_url); + EXPECT_EQ(captcha_token, out_captcha_token); + } + + net::ResponseCookies cookies_; + GURL client_login_source_; + GURL issue_auth_token_source_; + GURL client_login_to_oauth2_source_; + GURL oauth2_token_source_; + GURL token_auth_source_; + GURL merge_session_source_; + GURL uberauth_token_source_; + GURL client_oauth_source_; + GURL oauth_login_gurl_; + TestingProfile profile_; + protected: + MessageLoop message_loop_; +}; + +class MockGaiaConsumer : public GaiaAuthConsumer { + public: + MockGaiaConsumer() {} + ~MockGaiaConsumer() {} + + MOCK_METHOD1(OnClientLoginSuccess, void(const ClientLoginResult& result)); + MOCK_METHOD2(OnIssueAuthTokenSuccess, void(const std::string& service, + const std::string& token)); + MOCK_METHOD1(OnClientOAuthSuccess, + void(const GaiaAuthConsumer::ClientOAuthResult& result)); + MOCK_METHOD1(OnMergeSessionSuccess, void(const std::string& data)); + MOCK_METHOD1(OnUberAuthTokenSuccess, void(const std::string& data)); + MOCK_METHOD1(OnClientLoginFailure, + void(const GoogleServiceAuthError& error)); + MOCK_METHOD2(OnIssueAuthTokenFailure, void(const std::string& service, + const GoogleServiceAuthError& error)); + MOCK_METHOD1(OnClientOAuthFailure, + void(const GoogleServiceAuthError& error)); + MOCK_METHOD1(OnMergeSessionFailure, void( + const GoogleServiceAuthError& error)); + MOCK_METHOD1(OnUberAuthTokenFailure, void( + const GoogleServiceAuthError& error)); +}; + +#if defined(OS_WIN) +#define MAYBE_ErrorComparator DISABLED_ErrorComparator +#else +#define MAYBE_ErrorComparator ErrorComparator +#endif + +TEST_F(GaiaAuthFetcherTest, MAYBE_ErrorComparator) { + GoogleServiceAuthError expected_error = + GoogleServiceAuthError::FromConnectionError(-101); + + GoogleServiceAuthError matching_error = + GoogleServiceAuthError::FromConnectionError(-101); + + EXPECT_TRUE(expected_error == matching_error); + + expected_error = GoogleServiceAuthError::FromConnectionError(6); + + EXPECT_FALSE(expected_error == matching_error); + + expected_error = GoogleServiceAuthError(GoogleServiceAuthError::NONE); + + EXPECT_FALSE(expected_error == matching_error); + + matching_error = GoogleServiceAuthError(GoogleServiceAuthError::NONE); + + EXPECT_TRUE(expected_error == matching_error); +} + +TEST_F(GaiaAuthFetcherTest, LoginNetFailure) { + int error_no = net::ERR_CONNECTION_RESET; + net::URLRequestStatus status(net::URLRequestStatus::FAILED, error_no); + + GoogleServiceAuthError expected_error = + GoogleServiceAuthError::FromConnectionError(error_no); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginFailure(expected_error)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + + MockFetcher mock_fetcher( + client_login_source_, status, 0, net::ResponseCookies(), std::string(), + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, TokenNetFailure) { + int error_no = net::ERR_CONNECTION_RESET; + net::URLRequestStatus status(net::URLRequestStatus::FAILED, error_no); + + GoogleServiceAuthError expected_error = + GoogleServiceAuthError::FromConnectionError(error_no); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnIssueAuthTokenFailure(_, expected_error)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + + MockFetcher mock_fetcher( + issue_auth_token_source_, status, 0, cookies_, std::string(), + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + + +TEST_F(GaiaAuthFetcherTest, LoginDenied) { + std::string data("Error=BadAuthentication"); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + + GoogleServiceAuthError expected_error( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginFailure(expected_error)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + + MockFetcher mock_fetcher( + client_login_source_, status, net::HTTP_FORBIDDEN, cookies_, data, + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, ParseRequest) { + RunParsingTest("SID=sid\nLSID=lsid\nAuth=auth\n", "sid", "lsid", "auth"); + RunParsingTest("LSID=lsid\nSID=sid\nAuth=auth\n", "sid", "lsid", "auth"); + RunParsingTest("SID=sid\nLSID=lsid\nAuth=auth", "sid", "lsid", "auth"); + RunParsingTest("SID=sid\nAuth=auth\n", "sid", "", "auth"); + RunParsingTest("LSID=lsid\nAuth=auth\n", "", "lsid", "auth"); + RunParsingTest("\nAuth=auth\n", "", "", "auth"); + RunParsingTest("SID=sid", "sid", "", ""); +} + +TEST_F(GaiaAuthFetcherTest, ParseErrorRequest) { + RunErrorParsingTest("Url=U\n" + "Error=E\n" + "CaptchaToken=T\n" + "CaptchaUrl=C\n", "E", "U", "C", "T"); + RunErrorParsingTest("CaptchaToken=T\n" + "Error=E\n" + "Url=U\n" + "CaptchaUrl=C\n", "E", "U", "C", "T"); + RunErrorParsingTest("\n\n\nCaptchaToken=T\n" + "\nError=E\n" + "\nUrl=U\n" + "CaptchaUrl=C\n", "E", "U", "C", "T"); +} + + +TEST_F(GaiaAuthFetcherTest, OnlineLogin) { + std::string data("SID=sid\nLSID=lsid\nAuth=auth\n"); + + GaiaAuthConsumer::ClientLoginResult result; + result.lsid = "lsid"; + result.sid = "sid"; + result.token = "auth"; + result.data = data; + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginSuccess(result)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher( + client_login_source_, status, net::HTTP_OK, cookies_, data, + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, WorkingIssueAuthToken) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnIssueAuthTokenSuccess(_, "token")) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher( + issue_auth_token_source_, status, net::HTTP_OK, cookies_, "token", + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, CheckTwoFactorResponse) { + std::string response = + base::StringPrintf("Error=BadAuthentication\n%s\n", + GaiaAuthFetcher::kSecondFactor); + EXPECT_TRUE(GaiaAuthFetcher::IsSecondFactorSuccess(response)); +} + +TEST_F(GaiaAuthFetcherTest, CheckNormalErrorCode) { + std::string response = "Error=BadAuthentication\n"; + EXPECT_FALSE(GaiaAuthFetcher::IsSecondFactorSuccess(response)); +} + +TEST_F(GaiaAuthFetcherTest, TwoFactorLogin) { + std::string response = base::StringPrintf("Error=BadAuthentication\n%s\n", + GaiaAuthFetcher::kSecondFactor); + + GoogleServiceAuthError error = + GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginFailure(error)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher( + client_login_source_, status, net::HTTP_FORBIDDEN, cookies_, response, + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, CaptchaParse) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Url=http://www.google.com/login/captcha\n" + "Error=CaptchaRequired\n" + "CaptchaToken=CCTOKEN\n" + "CaptchaUrl=Captcha?ctoken=CCTOKEN\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + + std::string token = "CCTOKEN"; + GURL image_url("http://accounts.google.com/Captcha?ctoken=CCTOKEN"); + GURL unlock_url("http://www.google.com/login/captcha"); + + EXPECT_EQ(error.state(), GoogleServiceAuthError::CAPTCHA_REQUIRED); + EXPECT_EQ(error.captcha().token, token); + EXPECT_EQ(error.captcha().image_url, image_url); + EXPECT_EQ(error.captcha().unlock_url, unlock_url); +} + +TEST_F(GaiaAuthFetcherTest, AccountDeletedError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=AccountDeleted\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DELETED); +} + +TEST_F(GaiaAuthFetcherTest, AccountDisabledError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=AccountDisabled\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DISABLED); +} + +TEST_F(GaiaAuthFetcherTest,BadAuthenticationError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=BadAuthentication\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); +} + +TEST_F(GaiaAuthFetcherTest,IncomprehensibleError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=Gobbledygook\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +TEST_F(GaiaAuthFetcherTest,ServiceUnavailableError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=ServiceUnavailable\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +TEST_F(GaiaAuthFetcherTest, OAuthAccountDeletedError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=adel\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DELETED); +} + +TEST_F(GaiaAuthFetcherTest, OAuthAccountDisabledError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=adis\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DISABLED); +} + +TEST_F(GaiaAuthFetcherTest, OAuthBadAuthenticationError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=badauth\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); +} + +TEST_F(GaiaAuthFetcherTest, OAuthServiceUnavailableError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=ire\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +TEST_F(GaiaAuthFetcherTest, FullLogin) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginSuccess(_)) + .Times(1); + + MockURLFetcherFactory<MockFetcher> factory; + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartClientLogin("username", + "password", + "service", + std::string(), + std::string(), + GaiaAuthFetcher::HostedAccountsAllowed); +} + +TEST_F(GaiaAuthFetcherTest, FullLoginFailure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginFailure(_)) + .Times(1); + + MockURLFetcherFactory<MockFetcher> factory; + factory.set_success(false); + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartClientLogin("username", + "password", + "service", + std::string(), + std::string(), + GaiaAuthFetcher::HostedAccountsAllowed); +} + +TEST_F(GaiaAuthFetcherTest, ClientFetchPending) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginSuccess(_)) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartClientLogin("username", + "password", + "service", + std::string(), + std::string(), + GaiaAuthFetcher::HostedAccountsAllowed); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + client_login_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, "SID=sid\nLSID=lsid\nAuth=auth\n", + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, FullTokenSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnIssueAuthTokenSuccess("service", "token")) + .Times(1); + + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartIssueAuthToken("sid", "lsid", "service"); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + issue_auth_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, "token", + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, FullTokenFailure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnIssueAuthTokenFailure("service", _)) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartIssueAuthToken("sid", "lsid", "service"); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + issue_auth_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_FORBIDDEN, cookies_, "", net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult("rt1", "at1", 3600))).Times(1); + + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartLsoForOAuthLoginTokenExchange("lso_token"); + net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); + EXPECT_TRUE(NULL != fetcher); + EXPECT_EQ(net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES, + fetcher->GetLoadFlags()); + + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeValidCookie); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher1( + client_login_to_oauth2_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies, "", + net::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher1); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher2( + oauth2_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, kGetTokenPairValidResponse, + net::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher2); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenWithCookies) { + MockGaiaConsumer consumer; + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartCookieForOAuthLoginTokenExchange("0"); + net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); + EXPECT_TRUE(NULL != fetcher); + EXPECT_EQ(net::LOAD_NORMAL, fetcher->GetLoadFlags()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenClientLoginToOAuth2Failure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthFailure(_)) + .Times(1); + + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartLsoForOAuthLoginTokenExchange("lso_token"); + + net::ResponseCookies cookies; + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + client_login_to_oauth2_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_FORBIDDEN, cookies, "", + net::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenOAuth2TokenPairFailure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthFailure(_)) + .Times(1); + + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartLsoForOAuthLoginTokenExchange("lso_token"); + + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeValidCookie); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher1( + client_login_to_oauth2_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies, "", + net::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher1); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher2( + oauth2_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_FORBIDDEN, cookies_, "", + net::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher2); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, MergeSessionSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnMergeSessionSuccess("<html></html>")) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartMergeSession("myubertoken"); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + merge_session_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, "<html></html>", net::URLFetcher::GET, + &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, MergeSessionSuccessRedirect) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnMergeSessionSuccess("<html></html>")) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartMergeSession("myubertoken"); + + // Make sure the fetcher created has the expected flags. Set its url() + // properties to reflect a redirect. + net::TestURLFetcher* test_fetcher = factory.GetFetcherByID(0); + EXPECT_TRUE(test_fetcher != NULL); + EXPECT_TRUE(test_fetcher->GetLoadFlags() == net::LOAD_NORMAL); + EXPECT_TRUE(auth.HasPendingFetch()); + + GURL final_url("http://www.google.com/CheckCookie"); + test_fetcher->set_url(final_url); + test_fetcher->set_status( + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0)); + test_fetcher->set_response_code(net::HTTP_OK); + test_fetcher->set_cookies(cookies_); + test_fetcher->SetResponseString("<html></html>"); + + auth.OnURLFetchComplete(test_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, UberAuthTokenSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnUberAuthTokenSuccess("uberToken")) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartTokenFetchForUberAuthExchange("myAccessToken"); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + uberauth_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, "uberToken", net::URLFetcher::POST, + &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, ParseClientLoginToOAuth2Response) { + { // No cookies. + std::string auth_code; + net::ResponseCookies cookies; + EXPECT_FALSE(GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + cookies, &auth_code)); + EXPECT_EQ("", auth_code); + } + { // Few cookies, nothing appropriate. + std::string auth_code; + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeCookieNoSecure); + cookies.push_back(kGetAuthCodeCookieNoHttpOnly); + cookies.push_back(kGetAuthCodeCookieNoOAuthCode); + EXPECT_FALSE(GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + cookies, &auth_code)); + EXPECT_EQ("", auth_code); + } + { // Few cookies, one of them is valid. + std::string auth_code; + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeCookieNoSecure); + cookies.push_back(kGetAuthCodeCookieNoHttpOnly); + cookies.push_back(kGetAuthCodeCookieNoOAuthCode); + cookies.push_back(kGetAuthCodeValidCookie); + EXPECT_TRUE(GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + cookies, &auth_code)); + EXPECT_EQ("test-code", auth_code); + } + { // Single valid cookie (like in real responses). + std::string auth_code; + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeValidCookie); + EXPECT_TRUE(GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + cookies, &auth_code)); + EXPECT_EQ("test-code", auth_code); + } +} + +TEST_F(GaiaAuthFetcherTest, ClientOAuthSuccess) { + MockURLFetcherFactory<MockFetcher> factory; + factory.set_results(kClientOAuthValidResponse); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult("rt1", "at1", 3600))).Times(1); + + GaiaAuthFetcher auth(&consumer, "tests", profile_.GetRequestContext()); + std::vector<std::string> scopes; + scopes.push_back(GaiaUrls::GetInstance()->oauth1_login_scope()); + scopes.push_back("https://some.other.scope.com"); + auth.StartClientOAuth("username", "password", scopes, "", "en"); + + scoped_ptr<base::Value> actual(base::JSONReader::Read(auth.request_body_)); + scoped_ptr<base::Value> expected(base::JSONReader::Read( + "{" + "\"email\": \"username\"," + "\"password\": \"password\"," + "\"scopes\": [\"https://www.google.com/accounts/OAuthLogin\"," + " \"https://some.other.scope.com\"]," + "\"oauth2_client_id\": \"77185425430.apps.googleusercontent.com\"," + "\"friendly_device_name\": \"tests\"," + "\"accepts_challenges\": [\"Captcha\", \"TwoStep\"]," + "\"locale\": \"en\"," + "\"fallback\": { \"name\": \"GetOAuth2Token\" }" + "}")); + EXPECT_TRUE(expected->Equals(actual.get())); +} + +TEST_F(GaiaAuthFetcherTest, ClientOAuthWithQuote) { + MockURLFetcherFactory<MockFetcher> factory; + factory.set_results(kClientOAuthValidResponse); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult("rt1", "at1", 3600))).Times(1); + + GaiaAuthFetcher auth(&consumer, "te\"sts", profile_.GetRequestContext()); + std::vector<std::string> scopes; + scopes.push_back("https://some.\"other.scope.com"); + auth.StartClientOAuth("user\"name", "pass\"word", scopes, "", "e\"n"); + + scoped_ptr<base::Value> actual(base::JSONReader::Read(auth.request_body_)); + scoped_ptr<base::Value> expected(base::JSONReader::Read( + "{" + "\"email\": \"user\\\"name\"," + "\"password\": \"pass\\\"word\"," + "\"scopes\": [\"https://some.\\\"other.scope.com\"]," + "\"oauth2_client_id\": \"77185425430.apps.googleusercontent.com\"," + "\"friendly_device_name\": \"te\\\"sts\"," + "\"accepts_challenges\": [\"Captcha\", \"TwoStep\"]," + "\"locale\": \"e\\\"n\"," + "\"fallback\": { \"name\": \"GetOAuth2Token\" }" + "}")); + EXPECT_TRUE(expected->Equals(actual.get())); +} + +TEST_F(GaiaAuthFetcherTest, ClientOAuthBadAuth) { + MockURLFetcherFactory<MockFetcher> factory; + factory.set_success(false); + factory.set_results("{" + " \"cause\" : \"BadAuthentication\"," + " \"fallback\" : {" + " \"name\" : \"Terminating\"," + " \"url\" : \"https://www.terminating.com\"" + " }" + "}"); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthFailure(_)) + .WillOnce(Invoke(ExpectBadAuth)); + + GaiaAuthFetcher auth(&consumer, "tests", profile_.GetRequestContext()); + std::vector<std::string> scopes; + scopes.push_back(GaiaUrls::GetInstance()->oauth1_login_scope()); + auth.StartClientOAuth("username", "password", scopes, "", "en"); +} + +TEST_F(GaiaAuthFetcherTest, ClientOAuthCaptchaChallenge) { + MockURLFetcherFactory<MockFetcher> factory; + factory.set_success(false); + factory.set_results("{" + " \"cause\" : \"NeedsAdditional\"," + " \"fallback\" : {" + " \"name\" : \"Terminating\"," + " \"url\" : \"https://www.terminating.com\"" + " }," + " \"challenge\" : {" + " \"name\" : \"Captcha\"," + " \"image_url\" : \"http://www.image.com/\"," + " \"image_width\" : 640," + " \"image_height\" : 480," + " \"audio_url\" : \"http://www.audio.com/\"," + " \"challenge_token\" : \"challengetokenblob\"" + " }" + "}"); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthFailure(_)) + .WillOnce(Invoke(ExpectCaptchaChallenge)); + + GaiaAuthFetcher auth(&consumer, "tests", profile_.GetRequestContext()); + std::vector<std::string> scopes; + scopes.push_back(GaiaUrls::GetInstance()->oauth1_login_scope()); + auth.StartClientOAuth("username", "password", scopes, "", "en"); +} + +TEST_F(GaiaAuthFetcherTest, ClientOAuthTwoFactorChallenge) { + MockURLFetcherFactory<MockFetcher> factory; + factory.set_success(false); + factory.set_results("{" + " \"cause\" : \"NeedsAdditional\"," + " \"fallback\" : {" + " \"name\" : \"Terminating\"," + " \"url\" : \"https://www.terminating.com\"" + " }," + " \"challenge\" : {" + " \"name\" : \"TwoStep\"," + " \"prompt_text\" : \"prompt_text\"," + " \"alternate_text\" : \"alternate_text\"," + " \"challenge_token\" : \"challengetokenblob\"," + " \"field_length\" : 10" + " }" + "}"); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthFailure(_)) + .WillOnce(Invoke(ExpectTwoFactorChallenge)); + + GaiaAuthFetcher auth(&consumer, "tests", profile_.GetRequestContext()); + std::vector<std::string> scopes; + scopes.push_back(GaiaUrls::GetInstance()->oauth1_login_scope()); + auth.StartClientOAuth("username", "password", scopes, "", "en"); +} + +TEST_F(GaiaAuthFetcherTest, ClientOAuthChallengeSuccess) { + MockURLFetcherFactory<MockFetcher> factory; + factory.set_results(kClientOAuthValidResponse); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult("rt1", "at1", 3600))).Times(2); + + GaiaAuthFetcher auth1(&consumer, std::string(), profile_.GetRequestContext()); + auth1.StartClientOAuthChallengeResponse(GoogleServiceAuthError::TWO_FACTOR, + "token", "mysolution"); + + scoped_ptr<base::Value> actual1(base::JSONReader::Read(auth1.request_body_)); + scoped_ptr<base::Value> expected1(base::JSONReader::Read( + "{" + " \"challenge_reply\" : {" + " \"name\" : \"TwoStep\"," + " \"challenge_token\" : \"token\"," + " \"otp\" : \"mysolution\"" + " }" + "}")); + EXPECT_TRUE(expected1->Equals(actual1.get())); + + GaiaAuthFetcher auth2(&consumer, "tests", profile_.GetRequestContext()); + auth2.StartClientOAuthChallengeResponse( + GoogleServiceAuthError::CAPTCHA_REQUIRED, "token", "mysolution"); + + scoped_ptr<base::Value> actual2(base::JSONReader::Read(auth2.request_body_)); + scoped_ptr<base::Value> expected2(base::JSONReader::Read( + "{" + " \"challenge_reply\" : {" + " \"name\" : \"Captcha\"," + " \"challenge_token\" : \"token\"," + " \"solution\" : \"mysolution\"" + " }" + "}")); + EXPECT_TRUE(expected2->Equals(actual2.get())); +} + +TEST_F(GaiaAuthFetcherTest, ClientOAuthChallengeQuote) { + MockURLFetcherFactory<MockFetcher> factory; + factory.set_results(kClientOAuthValidResponse); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult("rt1", "at1", 3600))).Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), profile_.GetRequestContext()); + auth.StartClientOAuthChallengeResponse(GoogleServiceAuthError::TWO_FACTOR, + "to\"ken", "my\"solution"); + + scoped_ptr<base::Value> actual(base::JSONReader::Read(auth.request_body_)); + scoped_ptr<base::Value> expected(base::JSONReader::Read( + "{" + " \"challenge_reply\" : {" + " \"name\" : \"TwoStep\"," + " \"challenge_token\" : \"to\\\"ken\"," + " \"otp\" : \"my\\\"solution\"" + " }" + "}")); + EXPECT_TRUE(expected->Equals(actual.get())); +} + +TEST_F(GaiaAuthFetcherTest, StartOAuthLogin) { + // OAuthLogin returns the same as the ClientLogin endpoint, minus CAPTCHA + // responses. + std::string data("SID=sid\nLSID=lsid\nAuth=auth\n"); + + GaiaAuthConsumer::ClientLoginResult result; + result.lsid = "lsid"; + result.sid = "sid"; + result.token = "auth"; + result.data = data; + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginSuccess(result)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher( + oauth_login_gurl_, status, net::HTTP_OK, cookies_, data, + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} diff --git a/google_apis/gaia/gaia_auth_util.cc b/google_apis/gaia/gaia_auth_util.cc new file mode 100644 index 0000000..d8cac90 --- /dev/null +++ b/google_apis/gaia/gaia_auth_util.cc @@ -0,0 +1,61 @@ +// 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 "google_apis/gaia/gaia_auth_util.h" + +#include <vector> + +#include "base/logging.h" +#include "base/string_split.h" +#include "base/string_util.h" + +namespace gaia { + +namespace { +const char kGmailDomain[] = "gmail.com"; +} + +std::string CanonicalizeEmail(const std::string& email_address) { + std::vector<std::string> parts; + char at = '@'; + base::SplitString(email_address, at, &parts); + if (parts.size() != 2U) + NOTREACHED() << "expecting exactly one @, but got " << parts.size(); + else if (parts[1] == kGmailDomain) // only strip '.' for gmail accounts. + RemoveChars(parts[0], ".", &parts[0]); + std::string new_email = StringToLowerASCII(JoinString(parts, at)); + VLOG(1) << "Canonicalized " << email_address << " to " << new_email; + return new_email; +} + +std::string CanonicalizeDomain(const std::string& domain) { + // Canonicalization of domain names means lower-casing them. Make sure to + // update this function in sync with Canonicalize if this ever changes. + return StringToLowerASCII(domain); +} + +std::string SanitizeEmail(const std::string& email_address) { + std::string sanitized(email_address); + + // Apply a default domain if necessary. + if (sanitized.find('@') == std::string::npos) { + sanitized += '@'; + sanitized += kGmailDomain; + } + + return sanitized; +} + +std::string ExtractDomainName(const std::string& email_address) { + // First canonicalize which will also verify we have proper domain part. + std::string email = CanonicalizeEmail(email_address); + size_t separator_pos = email.find('@'); + if (separator_pos != email.npos && separator_pos < email.length() - 1) + return email.substr(separator_pos + 1); + else + NOTREACHED() << "Not a proper email address: " << email; + return std::string(); +} + +} // namespace gaia diff --git a/google_apis/gaia/gaia_auth_util.h b/google_apis/gaia/gaia_auth_util.h new file mode 100644 index 0000000..792a59b --- /dev/null +++ b/google_apis/gaia/gaia_auth_util.h @@ -0,0 +1,31 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ +#define GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ + +#include <string> + +namespace gaia { + +// Perform basic canonicalization of |email_address|, taking into account that +// gmail does not consider '.' or caps inside a username to matter. It also +// ignores everything after a '+'. For example, c.masone+abc@gmail.com == +// cMaSone@gmail.com, per +// http://mail.google.com/support/bin/answer.py?hl=en&ctx=mail&answer=10313# +std::string CanonicalizeEmail(const std::string& email_address); + +// Returns the canonical form of the given domain. +std::string CanonicalizeDomain(const std::string& domain); + +// Sanitize emails. Currently, it only ensures all emails have a domain by +// adding gmail.com if no domain is present. +std::string SanitizeEmail(const std::string& email_address); + +// Extract the domain part from the canonical form of the given email. +std::string ExtractDomainName(const std::string& email); + +} // namespace gaia + +#endif // GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ diff --git a/google_apis/gaia/gaia_auth_util_unittest.cc b/google_apis/gaia/gaia_auth_util_unittest.cc new file mode 100644 index 0000000..352c032 --- /dev/null +++ b/google_apis/gaia/gaia_auth_util_unittest.cc @@ -0,0 +1,87 @@ +// 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 "google_apis/gaia/gaia_auth_util.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace gaia { + +TEST(GaiaAuthUtilTest, EmailAddressNoOp) { + const char lower_case[] = "user@what.com"; + EXPECT_EQ(lower_case, CanonicalizeEmail(lower_case)); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreCaps) { + EXPECT_EQ(CanonicalizeEmail("user@what.com"), + CanonicalizeEmail("UsEr@what.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreDomainCaps) { + EXPECT_EQ(CanonicalizeEmail("user@what.com"), + CanonicalizeEmail("UsEr@what.COM")); +} + +TEST(GaiaAuthUtilTest, EmailAddressRejectOneUsernameDot) { + EXPECT_NE(CanonicalizeEmail("u.ser@what.com"), + CanonicalizeEmail("UsEr@what.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressMatchWithOneUsernameDot) { + EXPECT_EQ(CanonicalizeEmail("u.ser@what.com"), + CanonicalizeEmail("U.sEr@what.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreOneUsernameDot) { + EXPECT_EQ(CanonicalizeEmail("us.er@gmail.com"), + CanonicalizeEmail("UsEr@gmail.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreManyUsernameDots) { + EXPECT_EQ(CanonicalizeEmail("u.ser@gmail.com"), + CanonicalizeEmail("Us.E.r@gmail.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreConsecutiveUsernameDots) { + EXPECT_EQ(CanonicalizeEmail("use.r@gmail.com"), + CanonicalizeEmail("Us....E.r@gmail.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressDifferentOnesRejected) { + EXPECT_NE(CanonicalizeEmail("who@what.com"), + CanonicalizeEmail("Us....E.r@what.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnorePlusSuffix) { + const char with_plus[] = "user+cc@what.com"; + EXPECT_EQ(with_plus, CanonicalizeEmail(with_plus)); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreMultiPlusSuffix) { + const char multi_plus[] = "user+cc+bcc@what.com"; + EXPECT_EQ(multi_plus, CanonicalizeEmail(multi_plus)); +} + +TEST(GaiaAuthUtilTest, CanonicalizeDomain) { + const char domain[] = "example.com"; + EXPECT_EQ(domain, CanonicalizeDomain("example.com")); + EXPECT_EQ(domain, CanonicalizeDomain("EXAMPLE.cOm")); +} + +TEST(GaiaAuthUtilTest, ExtractDomainName) { + const char domain[] = "example.com"; + EXPECT_EQ(domain, ExtractDomainName("who@example.com")); + EXPECT_EQ(domain, ExtractDomainName("who@EXAMPLE.cOm")); +} + +TEST(GaiaAuthUtilTest, SanitizeMissingDomain) { + EXPECT_EQ("nodomain@gmail.com", SanitizeEmail("nodomain")); +} + +TEST(GaiaAuthUtilTest, SanitizeExistingDomain) { + const char existing[] = "test@example.com"; + EXPECT_EQ(existing, SanitizeEmail(existing)); +} + +} // namespace gaia diff --git a/google_apis/gaia/gaia_authenticator.cc b/google_apis/gaia/gaia_authenticator.cc new file mode 100644 index 0000000..f9cfa4e --- /dev/null +++ b/google_apis/gaia/gaia_authenticator.cc @@ -0,0 +1,400 @@ +// 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 "google_apis/gaia/gaia_authenticator.h" + +#include <string> +#include <utility> +#include <vector> + +#include "base/basictypes.h" +#include "base/port.h" +#include "base/string_split.h" +#include "googleurl/src/gurl.h" +#include "net/base/escape.h" +#include "net/http/http_status_code.h" + +using std::pair; +using std::string; +using std::vector; + +namespace gaia { + +static const char kGaiaV1IssueAuthTokenPath[] = "/accounts/IssueAuthToken"; + +static const char kGetUserInfoPath[] = "/accounts/GetUserInfo"; + +GaiaAuthenticator::AuthResults::AuthResults() : auth_error(None) {} + +GaiaAuthenticator::AuthResults::AuthResults(const AuthResults& other) + : email(other.email), + password(other.password), + sid(other.sid), + lsid(other.lsid), + auth_token(other.auth_token), + primary_email(other.primary_email), + error_msg(other.error_msg), + auth_error(other.auth_error), + auth_error_url(other.auth_error_url), + captcha_token(other.captcha_token), + captcha_url(other.captcha_url) { +} + +GaiaAuthenticator::AuthResults::~AuthResults() {} + +GaiaAuthenticator::AuthParams::AuthParams() : authenticator(NULL), + request_id(0) {} + +GaiaAuthenticator::AuthParams::~AuthParams() {} + +// Sole constructor with initializers for all fields. +GaiaAuthenticator::GaiaAuthenticator(const string& user_agent, + const string& service_id, + const string& gaia_url) + : user_agent_(user_agent), + service_id_(service_id), + gaia_url_(gaia_url), + request_count_(0), + delay_(0), + next_allowed_auth_attempt_time_(0), + early_auth_attempt_count_(0), + message_loop_(NULL) { +} + +GaiaAuthenticator::~GaiaAuthenticator() { +} + +// mutex_ must be entered before calling this function. +GaiaAuthenticator::AuthParams GaiaAuthenticator::MakeParams( + const string& user_name, + const string& password, + const string& captcha_token, + const string& captcha_value) { + AuthParams params; + params.request_id = ++request_count_; + params.email = user_name; + params.password = password; + params.captcha_token = captcha_token; + params.captcha_value = captcha_value; + params.authenticator = this; + return params; +} + +bool GaiaAuthenticator::Authenticate(const string& user_name, + const string& password, + const string& captcha_token, + const string& captcha_value) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + + AuthParams const params = + MakeParams(user_name, password, captcha_token, captcha_value); + return AuthenticateImpl(params); +} + +bool GaiaAuthenticator::AuthenticateWithLsid(const string& lsid) { + auth_results_.lsid = lsid; + // We need to lookup the email associated with this LSID cookie in order to + // update |auth_results_| with the correct values. + if (LookupEmail(&auth_results_)) { + auth_results_.email = auth_results_.primary_email; + return IssueAuthToken(&auth_results_, service_id_); + } + return false; +} + +bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + AuthResults results; + const bool succeeded = AuthenticateImpl(params, &results); + return succeeded; +} + +// This method makes an HTTP request to the Gaia server, and calls other +// methods to help parse the response. If authentication succeeded, then +// Gaia-issued cookies are available in the respective variables; if +// authentication failed, then the exact error is available as an enum. If the +// client wishes to save the credentials, the last parameter must be true. +// If a subsequent request is made with fresh credentials, the saved credentials +// are wiped out; any subsequent request to the zero-parameter overload of this +// method preserves the saved credentials. +bool GaiaAuthenticator::AuthenticateImpl(const AuthParams& params, + AuthResults* results) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + results->auth_error = ConnectionUnavailable; + results->email = params.email.data(); + results->password = params.password; + + // The aim of this code is to start failing requests if due to a logic error + // in the program we're hammering GAIA. +#if defined(OS_WIN) + __time32_t now = _time32(0); +#else // defined(OS_WIN) + time_t now = time(0); +#endif // defined(OS_WIN) + + if (now > next_allowed_auth_attempt_time_) { + next_allowed_auth_attempt_time_ = now + 1; + // If we're more than 2 minutes past the allowed time we reset the early + // attempt count. + if (now - next_allowed_auth_attempt_time_ > 2 * 60) { + delay_ = 1; + early_auth_attempt_count_ = 0; + } + } else { + ++early_auth_attempt_count_; + // Allow 3 attempts, but then limit. + if (early_auth_attempt_count_ > 3) { + delay_ = GetBackoffDelaySeconds(delay_); + next_allowed_auth_attempt_time_ = now + delay_; + return false; + } + } + + return PerformGaiaRequest(params, results); +} + +bool GaiaAuthenticator::PerformGaiaRequest(const AuthParams& params, + AuthResults* results) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + GURL gaia_auth_url(gaia_url_); + + string post_body; + post_body += "Email=" + net::EscapeUrlEncodedData(params.email, true); + post_body += "&Passwd=" + net::EscapeUrlEncodedData(params.password, true); + post_body += "&source=" + net::EscapeUrlEncodedData(user_agent_, true); + post_body += "&service=" + service_id_; + if (!params.captcha_token.empty() && !params.captcha_value.empty()) { + post_body += "&logintoken=" + + net::EscapeUrlEncodedData(params.captcha_token, true); + post_body += "&logincaptcha=" + + net::EscapeUrlEncodedData(params.captcha_value, true); + } + post_body += "&PersistentCookie=true"; + // We set it to GOOGLE (and not HOSTED or HOSTED_OR_GOOGLE) because we only + // allow consumer logins. + post_body += "&accountType=GOOGLE"; + + string message_text; + unsigned long server_response_code; + if (!Post(gaia_auth_url, post_body, &server_response_code, &message_text)) { + results->auth_error = ConnectionUnavailable; + return false; + } + + // Parse reply in two different ways, depending on if request failed or + // succeeded. + if (net::HTTP_FORBIDDEN == server_response_code) { + ExtractAuthErrorFrom(message_text, results); + return false; + } else if (net::HTTP_OK == server_response_code) { + ExtractTokensFrom(message_text, results); + if (!IssueAuthToken(results, service_id_)) { + return false; + } + + return LookupEmail(results); + } else { + results->auth_error = Unknown; + return false; + } +} + +bool GaiaAuthenticator::Post(const GURL& url, + const std::string& post_body, + unsigned long* response_code, + std::string* response_body) { + return false; +} + +bool GaiaAuthenticator::LookupEmail(AuthResults* results) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + // Use the provided Gaia server, but change the path to what V1 expects. + GURL url(gaia_url_); // Gaia server. + GURL::Replacements repl; + // Needs to stay in scope till GURL is out of scope. + string path(kGetUserInfoPath); + repl.SetPathStr(path); + url = url.ReplaceComponents(repl); + + string post_body; + post_body += "LSID="; + post_body += net::EscapeUrlEncodedData(results->lsid, true); + + unsigned long server_response_code; + string message_text; + if (!Post(url, post_body, &server_response_code, &message_text)) { + return false; + } + + // Check if we received a valid AuthToken; if not, ignore it. + if (net::HTTP_FORBIDDEN == server_response_code) { + // Server says we're not authenticated. + ExtractAuthErrorFrom(message_text, results); + return false; + } else if (net::HTTP_OK == server_response_code) { + typedef vector<pair<string, string> > Tokens; + Tokens tokens; + base::SplitStringIntoKeyValuePairs(message_text, '=', '\n', &tokens); + for (Tokens::iterator i = tokens.begin(); i != tokens.end(); ++i) { + if ("accountType" == i->first) { + // We never authenticate an email as a hosted account. + DCHECK_EQ("GOOGLE", i->second); + } else if ("email" == i->first) { + results->primary_email = i->second; + } + } + return true; + } + return false; +} + +int GaiaAuthenticator::GetBackoffDelaySeconds(int current_backoff_delay) { + NOTREACHED(); + return current_backoff_delay; +} + +// We need to call this explicitly when we need to obtain a long-lived session +// token. +bool GaiaAuthenticator::IssueAuthToken(AuthResults* results, + const string& service_id) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + // Use the provided Gaia server, but change the path to what V1 expects. + GURL url(gaia_url_); // Gaia server. + GURL::Replacements repl; + // Needs to stay in scope till GURL is out of scope. + string path(kGaiaV1IssueAuthTokenPath); + repl.SetPathStr(path); + url = url.ReplaceComponents(repl); + + string post_body; + post_body += "LSID="; + post_body += net::EscapeUrlEncodedData(results->lsid, true); + post_body += "&service=" + service_id; + post_body += "&Session=true"; + + unsigned long server_response_code; + string message_text; + if (!Post(url, post_body, &server_response_code, &message_text)) { + return false; + } + + // Check if we received a valid AuthToken; if not, ignore it. + if (net::HTTP_FORBIDDEN == server_response_code) { + // Server says we're not authenticated. + ExtractAuthErrorFrom(message_text, results); + return false; + } else if (net::HTTP_OK == server_response_code) { + // Note that the format of message_text is different from what is returned + // in the first request, or to the sole request that is made to Gaia V2. + // Specifically, the entire string is the AuthToken, and looks like: + // "<token>" rather than "AuthToken=<token>". Thus, we need not use + // ExtractTokensFrom(...), but simply assign the token. + int last_index = message_text.length() - 1; + if ('\n' == message_text[last_index]) + message_text.erase(last_index); + results->auth_token = message_text; + return true; + } + return false; +} + +// Helper method that extracts tokens from a successful reply, and saves them +// in the right fields. +void GaiaAuthenticator::ExtractTokensFrom(const string& response, + AuthResults* results) { + vector<pair<string, string> > tokens; + base::SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens); + for (vector<pair<string, string> >::iterator i = tokens.begin(); + i != tokens.end(); ++i) { + if (i->first == "SID") { + results->sid = i->second; + } else if (i->first == "LSID") { + results->lsid = i->second; + } else if (i->first == "Auth") { + results->auth_token = i->second; + } + } +} + +// Helper method that extracts tokens from a failure response, and saves them +// in the right fields. +void GaiaAuthenticator::ExtractAuthErrorFrom(const string& response, + AuthResults* results) { + vector<pair<string, string> > tokens; + base::SplitStringIntoKeyValuePairs(response, '=', '\n', &tokens); + for (vector<pair<string, string> >::iterator i = tokens.begin(); + i != tokens.end(); ++i) { + if (i->first == "Error") { + results->error_msg = i->second; + } else if (i->first == "Url") { + results->auth_error_url = i->second; + } else if (i->first == "CaptchaToken") { + results->captcha_token = i->second; + } else if (i->first == "CaptchaUrl") { + results->captcha_url = i->second; + } + } + + // Convert string error messages to enum values. Each case has two different + // strings; the first one is the most current and the second one is + // deprecated, but available. + const string& error_msg = results->error_msg; + if (error_msg == "BadAuthentication" || error_msg == "badauth") { + results->auth_error = BadAuthentication; + } else if (error_msg == "NotVerified" || error_msg == "nv") { + results->auth_error = NotVerified; + } else if (error_msg == "TermsNotAgreed" || error_msg == "tna") { + results->auth_error = TermsNotAgreed; + } else if (error_msg == "Unknown" || error_msg == "unknown") { + results->auth_error = Unknown; + } else if (error_msg == "AccountDeleted" || error_msg == "adel") { + results->auth_error = AccountDeleted; + } else if (error_msg == "AccountDisabled" || error_msg == "adis") { + results->auth_error = AccountDisabled; + } else if (error_msg == "CaptchaRequired" || error_msg == "cr") { + results->auth_error = CaptchaRequired; + } else if (error_msg == "ServiceUnavailable" || error_msg == "ire") { + results->auth_error = ServiceUnavailable; + } +} + +// Reset all stored credentials, perhaps in preparation for letting a different +// user sign in. +void GaiaAuthenticator::ResetCredentials() { + DCHECK_EQ(MessageLoop::current(), message_loop_); + AuthResults blank; + auth_results_ = blank; +} + +void GaiaAuthenticator::SetUsernamePassword(const string& username, + const string& password) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + auth_results_.password = password; + auth_results_.email = username; +} + +void GaiaAuthenticator::SetUsername(const string& username) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + auth_results_.email = username; +} + +void GaiaAuthenticator::RenewAuthToken(const string& auth_token) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + DCHECK(!this->auth_token().empty()); + auth_results_.auth_token = auth_token; +} +void GaiaAuthenticator::SetAuthToken(const string& auth_token) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + auth_results_.auth_token = auth_token; +} + +bool GaiaAuthenticator::Authenticate(const string& user_name, + const string& password) { + DCHECK_EQ(MessageLoop::current(), message_loop_); + const string empty; + return Authenticate(user_name, password, empty, + empty); +} + +} // namespace gaia diff --git a/google_apis/gaia/gaia_authenticator.h b/google_apis/gaia/gaia_authenticator.h new file mode 100644 index 0000000..4aba67c --- /dev/null +++ b/google_apis/gaia/gaia_authenticator.h @@ -0,0 +1,273 @@ +// Copyright (c) 2011 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. +// +// Use this class to authenticate users with Gaia and access cookies sent +// by the Gaia servers. This class cannot be used on its own becaue it relies +// on a subclass to provide the virtual Post and GetBackoffDelaySeconds methods. +// +// Sample usage: +// class ActualGaiaAuthenticator : public gaia::GaiaAuthenticator { +// Provides actual implementation of Post and GetBackoffDelaySeconds. +// }; +// ActualGaiaAuthenticator gaia_auth("User-Agent", SERVICE_NAME, kGaiaUrl); +// if (gaia_auth.Authenticate("email", "passwd", SAVE_IN_MEMORY_ONLY, +// true)) { // Synchronous +// // Do something with: gaia_auth.auth_token(), or gaia_auth.sid(), +// // or gaia_auth.lsid() +// } +// +// Credentials can also be preserved for subsequent requests, though these are +// saved in plain-text in memory, and not very secure on client systems. The +// email address associated with the Gaia account can be read; the password is +// write-only. + +// TODO(sanjeevr): This class has been moved here from the bookmarks sync code. +// While it is a generic class that handles GAIA authentication, there are some +// artifacts of the sync code which needs to be cleaned up. +#ifndef GOOGLE_APIS_GAIA_GAIA_AUTHENTICATOR_H_ +#define GOOGLE_APIS_GAIA_GAIA_AUTHENTICATOR_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/gtest_prod_util.h" +#include "base/message_loop.h" +#include "googleurl/src/gurl.h" + +namespace gaia { + +// Error codes from Gaia. These will be set correctly for both Gaia V1 +// (/ClientAuth) and V2 (/ClientLogin) +enum AuthenticationError { + None = 0, + BadAuthentication = 1, + NotVerified = 2, + TermsNotAgreed = 3, + Unknown = 4, + AccountDeleted = 5, + AccountDisabled = 6, + CaptchaRequired = 7, + ServiceUnavailable = 8, + // Errors generated by this class not Gaia. + CredentialsNotSet = 9, + ConnectionUnavailable = 10 +}; + +class GaiaAuthenticator; + +// GaiaAuthenticator can be used to pass user credentials to Gaia and obtain +// cookies set by the Gaia servers. +class GaiaAuthenticator { + FRIEND_TEST_ALL_PREFIXES(GaiaAuthenticatorTest, + TestNewlineAtEndOfAuthTokenRemoved); + public: + + // Since GaiaAuthenticator can be used for any service, or by any client, you + // must include a user-agent and a service-id when creating one. The + // user_agent is a short string used for simple log analysis. gaia_url is used + // to choose the server to authenticate with (e.g. + // http://accounts.google.com/ClientLogin). + GaiaAuthenticator(const std::string& user_agent, + const std::string& service_id, + const std::string& gaia_url); + + virtual ~GaiaAuthenticator(); + + // This object should only be invoked from the AuthWatcherThread message + // loop, which is injected here. + void set_message_loop(const MessageLoop* loop) { + message_loop_ = loop; + } + + // Pass credentials to authenticate with, or use saved credentials via an + // overload. If authentication succeeds, you can retrieve the authentication + // token via the respective accessors. Returns a boolean indicating whether + // authentication succeeded or not. + bool Authenticate(const std::string& user_name, const std::string& password, + const std::string& captcha_token, + const std::string& captcha_value); + + bool Authenticate(const std::string& user_name, const std::string& password); + + // Pass the LSID to authenticate with. If the authentication succeeds, you can + // retrieve the authetication token via the respective accessors. Returns a + // boolean indicating whether authentication succeeded or not. + // Always returns a long lived token. + bool AuthenticateWithLsid(const std::string& lsid); + + // Resets all stored cookies to their default values. + void ResetCredentials(); + + void SetUsernamePassword(const std::string& username, + const std::string& password); + + void SetUsername(const std::string& username); + + // Virtual for testing + virtual void RenewAuthToken(const std::string& auth_token); + void SetAuthToken(const std::string& auth_token); + + struct AuthResults { + AuthResults(); + AuthResults(const AuthResults& other); + ~AuthResults(); + + std::string email; + std::string password; + + // Fields that store various cookies. + std::string sid; + std::string lsid; + std::string auth_token; + + std::string primary_email; + + // Fields for items returned when authentication fails. + std::string error_msg; + enum AuthenticationError auth_error; + std::string auth_error_url; + std::string captcha_token; + std::string captcha_url; + }; + + protected: + + struct AuthParams { + AuthParams(); + ~AuthParams(); + + GaiaAuthenticator* authenticator; + uint32 request_id; + std::string email; + std::string password; + std::string captcha_token; + std::string captcha_value; + }; + + // mutex_ must be entered before calling this function. + AuthParams MakeParams(const std::string& user_name, + const std::string& password, + const std::string& captcha_token, + const std::string& captcha_value); + + // The real Authenticate implementations. + bool AuthenticateImpl(const AuthParams& params); + bool AuthenticateImpl(const AuthParams& params, AuthResults* results); + + // virtual for testing purposes. + virtual bool PerformGaiaRequest(const AuthParams& params, + AuthResults* results); + virtual bool Post(const GURL& url, const std::string& post_body, + unsigned long* response_code, std::string* response_body); + + // Caller should fill in results->LSID before calling. Result in + // results->primary_email. + virtual bool LookupEmail(AuthResults* results); + + // Subclasses must override to provide a backoff delay. It is virtual instead + // of pure virtual for testing purposes. + // TODO(sanjeevr): This should be made pure virtual. But this class is + // currently directly being used in sync/engine/authenticator.cc, which is + // wrong. + virtual int GetBackoffDelaySeconds(int current_backoff_delay); + + public: + // Retrieve email. + inline std::string email() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.email; + } + + // Retrieve password. + inline std::string password() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.password; + } + + // Retrieve AuthToken, if previously authenticated; otherwise returns "". + inline std::string auth_token() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.auth_token; + } + + // Retrieve SID cookie. For details, see the Google Accounts documentation. + inline std::string sid() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.sid; + } + + // Retrieve LSID cookie. For details, see the Google Accounts documentation. + inline std::string lsid() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.lsid; + } + + // Get last authentication error. + inline enum AuthenticationError auth_error() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.auth_error; + } + + inline std::string auth_error_url() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.auth_error_url; + } + + inline std::string captcha_token() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.captcha_token; + } + + inline std::string captcha_url() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_.captcha_url; + } + + inline AuthResults results() const { + DCHECK_EQ(MessageLoop::current(), message_loop_); + return auth_results_; + } + + private: + bool IssueAuthToken(AuthResults* results, const std::string& service_id); + + // Helper method to parse response when authentication succeeds. + void ExtractTokensFrom(const std::string& response, AuthResults* results); + // Helper method to parse response when authentication fails. + void ExtractAuthErrorFrom(const std::string& response, AuthResults* results); + + // Fields for the obvious data items. + const std::string user_agent_; + const std::string service_id_; + const std::string gaia_url_; + + AuthResults auth_results_; + + // When multiple async requests are running, only the one that started most + // recently updates the values. + // + // Note that even though this code was written to handle multiple requests + // simultaneously, the sync code issues auth requests one at a time. + uint32 request_count_; + + // Used to compute backoff time for next allowed authentication. + int delay_; // In seconds. + // On Windows, time_t is 64-bit by default. Even though we have defined the + // _USE_32BIT_TIME_T preprocessor flag, other libraries including this header + // may not have that preprocessor flag defined resulting in mismatched class + // sizes. So we explicitly define it as 32-bit on Windows. + // TODO(sanjeevr): Change this to to use base::Time +#if defined(OS_WIN) + __time32_t next_allowed_auth_attempt_time_; +#else // defined(OS_WIN) + time_t next_allowed_auth_attempt_time_; +#endif // defined(OS_WIN) + int early_auth_attempt_count_; + + // The message loop all our methods are invoked on. + const MessageLoop* message_loop_; +}; + +} // namespace gaia +#endif // GOOGLE_APIS_GAIA_GAIA_AUTHENTICATOR_H_ diff --git a/google_apis/gaia/gaia_authenticator_unittest.cc b/google_apis/gaia/gaia_authenticator_unittest.cc new file mode 100644 index 0000000..8f63193 --- /dev/null +++ b/google_apis/gaia/gaia_authenticator_unittest.cc @@ -0,0 +1,49 @@ +// 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 "google_apis/gaia/gaia_authenticator.h" + +#include <string> + +#include "googleurl/src/gurl.h" +#include "net/http/http_status_code.h" +#include "testing/gtest/include/gtest/gtest.h" + +using std::string; + +namespace gaia { + +class GaiaAuthenticatorTest : public testing::Test { }; + +class GaiaAuthMockForGaiaAuthenticator : public GaiaAuthenticator { + public: + GaiaAuthMockForGaiaAuthenticator() + : GaiaAuthenticator("useragent", "serviceid", "http://gaia_url") {} + ~GaiaAuthMockForGaiaAuthenticator() {} + protected: + bool Post(const GURL& url, const string& post_body, + unsigned long* response_code, string* response_body) { + *response_code = net::HTTP_OK; + response_body->assign("body\n"); + return true; + } + + int GetBackoffDelaySeconds( + int current_backoff_delay) { + // Dummy delay value. + return 5; + } +}; + +TEST(GaiaAuthenticatorTest, TestNewlineAtEndOfAuthTokenRemoved) { + GaiaAuthMockForGaiaAuthenticator mock_auth; + MessageLoop message_loop; + mock_auth.set_message_loop(&message_loop); + GaiaAuthenticator::AuthResults results; + EXPECT_TRUE(mock_auth.IssueAuthToken(&results, "sid")); + EXPECT_EQ(0, results.auth_token.compare("body")); +} + +} // namespace gaia + diff --git a/google_apis/gaia/gaia_constants.cc b/google_apis/gaia/gaia_constants.cc new file mode 100644 index 0000000..8a5910c --- /dev/null +++ b/google_apis/gaia/gaia_constants.cc @@ -0,0 +1,95 @@ +// 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. +// +// Constants definitions + +#include "google_apis/gaia/gaia_constants.h" + +namespace GaiaConstants { + +// Gaia uses this for accounting where login is coming from. +const char kChromeOSSource[] = "chromeos"; +const char kChromeSource[] = "ChromiumBrowser"; + +// Service name for Gaia. Used to convert to cookie auth. +const char kGaiaService[] = "gaia"; +// Service name for Picasa API. API is used to get user's image. +const char kPicasaService[] = "lh2"; + +// Service/scope names for sync. +const char kSyncService[] = "chromiumsync"; + +// Service name for remoting. +const char kRemotingService[] = "chromoting"; +// Service name for cloud print. +const char kCloudPrintService[] = "cloudprint"; + +// Service/scope names for device management (cloud-based policy) server. +const char kDeviceManagementService[] = "mobilesync"; +const char kDeviceManagementServiceOAuth[] = + "https://www.googleapis.com/auth/chromeosdevicemanagement"; + +// OAuth scopes for chrome web store. +const char kCWSNotificationScope[] = + "https://www.googleapis.com/auth/chromewebstore.notification"; + +// Service for LSO endpoint of Google that exposes OAuth APIs. +const char kLSOService[] = "lso"; + +// Used to mint uber auth tokens when needed. +const char kGaiaSid[] = "sid"; +const char kGaiaLsid[] = "lsid"; +const char kGaiaOAuthToken[] = "oauthToken"; +const char kGaiaOAuthSecret[] = "oauthSecret"; +const char kGaiaOAuthDuration[] = "3600"; +const char kGaiaOAuth2LoginRefreshToken[] = "oauth2LoginRefreshToken"; +const char kGaiaOAuth2LoginAccessToken[] = "oauth2LoginAccessToken"; + + +// Used to build ClientOAuth requests. These are the names of keys used when +// building base::DictionaryValue that represent the json data that makes up +// the ClientOAuth endpoint protocol. The comment above each constant explains +// what value is associated with that key. + +// Canonical email and password of the account to sign in. +const char kClientOAuthEmailKey[] = "email"; +const char kClientOAuthPasswordKey[] = "password"; + +// Scopes required for the returned oauth2 token. For GaiaAuthFetcher, the +// value is the OAuthLogin scope. +const char kClientOAuthScopesKey[] = "scopes"; + +// Chrome's client id from the API console. +const char kClientOAuthOAuth2ClientIdKey[] = "oauth2_client_id"; + +// A friendly name to describe this instance of chrome to the user. +const char kClientOAuthFriendlyDeviceNameKey[] = "friendly_device_name"; + +// A list of challenge types that chrome accepts. At a minimum this must +// include Captcha. To support OTPs should also include TwoFactor. +const char kClientOAuthAcceptsChallengesKey[] = "accepts_challenges"; + +// The locale of the browser, so that ClientOAuth can return localized error +// messages. +const char kClientOAuthLocaleKey[] = "locale"; + +// The name of the web-based fallback method to use if ClientOAuth decides it +// cannot continue otherwise. Note that this name has a dot because its in +// sub dictionary. +const char kClientOAuthFallbackNameKey[] = "fallback.name"; + +// The following three key names are used with ClientOAuth challenge responses. + +// The type of response. Must match the name given in the response to the +// original ClientOAuth request and is a subset of the challenge types listed +// in kClientOAuthAcceptsChallengesKey from that original request. +const char kClientOAuthNameKey[] = "name"; + +// The challenge token received in the original ClientOAuth request. +const char kClientOAuthChallengeTokenKey[] = "challenge_token"; + +// The dictionary that contains the challenge response. +const char kClientOAuthchallengeReplyKey[] = "challenge_reply"; + +} // namespace GaiaConstants diff --git a/google_apis/gaia/gaia_constants.h b/google_apis/gaia/gaia_constants.h new file mode 100644 index 0000000..a00635e --- /dev/null +++ b/google_apis/gaia/gaia_constants.h @@ -0,0 +1,53 @@ +// 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. +// +// Constants used by IssueAuthToken and ClientLogin + +#ifndef GOOGLE_APIS_GAIA_GAIA_CONSTANTS_H_ +#define GOOGLE_APIS_GAIA_GAIA_CONSTANTS_H_ + +namespace GaiaConstants { + +// Gaia sources for accounting +extern const char kChromeOSSource[]; +extern const char kChromeSource[]; + +// Gaia services for requesting +extern const char kGaiaService[]; // uber token +extern const char kPicasaService[]; +extern const char kSyncService[]; +extern const char kRemotingService[]; +extern const char kCloudPrintService[]; +extern const char kDeviceManagementService[]; +extern const char kDeviceManagementServiceOAuth[]; +extern const char kCWSService[]; +extern const char kCWSNotificationScope[]; +extern const char kLSOService[]; + +// Used with uber auth tokens when needed. +extern const char kGaiaSid[]; +extern const char kGaiaLsid[]; +extern const char kGaiaOAuthToken[]; +extern const char kGaiaOAuthSecret[]; +extern const char kGaiaOAuthDuration[]; +extern const char kGaiaOAuth2LoginRefreshToken[]; +extern const char kGaiaOAuth2LoginAccessToken[]; + +// Used to build ClientOAuth requests. These are the names of keys used in +// the json dictionaries that are sent in the protocol. +extern const char kClientOAuthEmailKey[]; +extern const char kClientOAuthPasswordKey[]; +extern const char kClientOAuthScopesKey[]; +extern const char kClientOAuthOAuth2ClientIdKey[]; +extern const char kClientOAuthFriendlyDeviceNameKey[]; +extern const char kClientOAuthAcceptsChallengesKey[]; +extern const char kClientOAuthLocaleKey[]; +extern const char kClientOAuthFallbackNameKey[]; +extern const char kClientOAuthNameKey[]; +extern const char kClientOAuthChallengeTokenKey[]; +extern const char kClientOAuthchallengeReplyKey[]; + +} // namespace GaiaConstants + +#endif // GOOGLE_APIS_GAIA_GAIA_CONSTANTS_H_ diff --git a/google_apis/gaia/gaia_oauth_client.cc b/google_apis/gaia/gaia_oauth_client.cc new file mode 100644 index 0000000..3ac819e --- /dev/null +++ b/google_apis/gaia/gaia_oauth_client.cc @@ -0,0 +1,208 @@ +// 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 "google_apis/gaia/gaia_oauth_client.h" + +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "googleurl/src/gurl.h" +#include "net/base/escape.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_context_getter.h" + +namespace { +const char kAccessTokenValue[] = "access_token"; +const char kRefreshTokenValue[] = "refresh_token"; +const char kExpiresInValue[] = "expires_in"; +} + +namespace gaia { + +class GaiaOAuthClient::Core + : public base::RefCountedThreadSafe<GaiaOAuthClient::Core>, + public net::URLFetcherDelegate { + public: + Core(const std::string& gaia_url, + net::URLRequestContextGetter* request_context_getter) + : gaia_url_(gaia_url), + num_retries_(0), + request_context_getter_(request_context_getter), + delegate_(NULL) {} + + void GetTokensFromAuthCode(const OAuthClientInfo& oauth_client_info, + const std::string& auth_code, + int max_retries, + GaiaOAuthClient::Delegate* delegate); + void RefreshToken(const OAuthClientInfo& oauth_client_info, + const std::string& refresh_token, + int max_retries, + GaiaOAuthClient::Delegate* delegate); + + // net::URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source); + + private: + friend class base::RefCountedThreadSafe<Core>; + virtual ~Core() {} + + void MakeGaiaRequest(const std::string& post_body, + int max_retries, + GaiaOAuthClient::Delegate* delegate); + void HandleResponse(const net::URLFetcher* source, + bool* should_retry_request); + + GURL gaia_url_; + int num_retries_; + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + GaiaOAuthClient::Delegate* delegate_; + scoped_ptr<net::URLFetcher> request_; +}; + +void GaiaOAuthClient::Core::GetTokensFromAuthCode( + const OAuthClientInfo& oauth_client_info, + const std::string& auth_code, + int max_retries, + GaiaOAuthClient::Delegate* delegate) { + std::string post_body = + "code=" + net::EscapeUrlEncodedData(auth_code, true) + + "&client_id=" + net::EscapeUrlEncodedData(oauth_client_info.client_id, + true) + + "&client_secret=" + + net::EscapeUrlEncodedData(oauth_client_info.client_secret, true) + + "&redirect_uri=oob&grant_type=authorization_code"; + MakeGaiaRequest(post_body, max_retries, delegate); +} + +void GaiaOAuthClient::Core::RefreshToken( + const OAuthClientInfo& oauth_client_info, + const std::string& refresh_token, + int max_retries, + GaiaOAuthClient::Delegate* delegate) { + std::string post_body = + "refresh_token=" + net::EscapeUrlEncodedData(refresh_token, true) + + "&client_id=" + net::EscapeUrlEncodedData(oauth_client_info.client_id, + true) + + "&client_secret=" + + net::EscapeUrlEncodedData(oauth_client_info.client_secret, true) + + "&grant_type=refresh_token"; + MakeGaiaRequest(post_body, max_retries, delegate); +} + +void GaiaOAuthClient::Core::MakeGaiaRequest( + const std::string& post_body, + int max_retries, + GaiaOAuthClient::Delegate* delegate) { + DCHECK(!request_.get()) << "Tried to fetch two things at once!"; + delegate_ = delegate; + num_retries_ = 0; + request_.reset(net::URLFetcher::Create( + 0, gaia_url_, net::URLFetcher::POST, this)); + request_->SetRequestContext(request_context_getter_); + request_->SetUploadData("application/x-www-form-urlencoded", post_body); + request_->SetMaxRetries(max_retries); + request_->Start(); +} + +// URLFetcher::Delegate implementation. +void GaiaOAuthClient::Core::OnURLFetchComplete( + const net::URLFetcher* source) { + bool should_retry = false; + HandleResponse(source, &should_retry); + if (should_retry) { + // Explicitly call ReceivedContentWasMalformed() to ensure the current + // request gets counted as a failure for calculation of the back-off + // period. If it was already a failure by status code, this call will + // be ignored. + request_->ReceivedContentWasMalformed(); + num_retries_++; + // We must set our request_context_getter_ again because + // URLFetcher::Core::RetryOrCompleteUrlFetch resets it to NULL... + request_->SetRequestContext(request_context_getter_); + request_->Start(); + } else { + request_.reset(); + } +} + +void GaiaOAuthClient::Core::HandleResponse( + const net::URLFetcher* source, + bool* should_retry_request) { + *should_retry_request = false; + // RC_BAD_REQUEST means the arguments are invalid. No point retrying. We are + // done here. + if (source->GetResponseCode() == net::HTTP_BAD_REQUEST) { + delegate_->OnOAuthError(); + return; + } + std::string access_token; + std::string refresh_token; + int expires_in_seconds = 0; + if (source->GetResponseCode() == net::HTTP_OK) { + std::string data; + source->GetResponseAsString(&data); + scoped_ptr<Value> message_value(base::JSONReader::Read(data)); + if (message_value.get() && + message_value->IsType(Value::TYPE_DICTIONARY)) { + scoped_ptr<DictionaryValue> response_dict( + static_cast<DictionaryValue*>(message_value.release())); + response_dict->GetString(kAccessTokenValue, &access_token); + response_dict->GetString(kRefreshTokenValue, &refresh_token); + response_dict->GetInteger(kExpiresInValue, &expires_in_seconds); + } + } + if (access_token.empty()) { + // If we don't have an access token yet and the the error was not + // RC_BAD_REQUEST, we may need to retry. + if ((-1 != source->GetMaxRetries()) && + (num_retries_ > source->GetMaxRetries())) { + // Retry limit reached. Give up. + delegate_->OnNetworkError(source->GetResponseCode()); + } else { + *should_retry_request = true; + } + } else if (refresh_token.empty()) { + // If we only have an access token, then this was a refresh request. + delegate_->OnRefreshTokenResponse(access_token, expires_in_seconds); + } else { + delegate_->OnGetTokensResponse(refresh_token, + access_token, + expires_in_seconds); + } +} + +GaiaOAuthClient::GaiaOAuthClient(const std::string& gaia_url, + net::URLRequestContextGetter* context_getter) { + core_ = new Core(gaia_url, context_getter); +} + +GaiaOAuthClient::~GaiaOAuthClient() { +} + + +void GaiaOAuthClient::GetTokensFromAuthCode( + const OAuthClientInfo& oauth_client_info, + const std::string& auth_code, + int max_retries, + Delegate* delegate) { + return core_->GetTokensFromAuthCode(oauth_client_info, + auth_code, + max_retries, + delegate); +} + +void GaiaOAuthClient::RefreshToken(const OAuthClientInfo& oauth_client_info, + const std::string& refresh_token, + int max_retries, + Delegate* delegate) { + return core_->RefreshToken(oauth_client_info, + refresh_token, + max_retries, + delegate); +} + +} // namespace gaia diff --git a/google_apis/gaia/gaia_oauth_client.h b/google_apis/gaia/gaia_oauth_client.h new file mode 100644 index 0000000..fff51cf --- /dev/null +++ b/google_apis/gaia/gaia_oauth_client.h @@ -0,0 +1,74 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_GAIA_OAUTH_CLIENT_H_ +#define GOOGLE_APIS_GAIA_GAIA_OAUTH_CLIENT_H_ + +#include <string> + +#include "base/memory/ref_counted.h" +#include "base/message_loop_proxy.h" + +namespace net { +class URLRequestContextGetter; +} + +// A helper class to get and refresh OAuth tokens given an authorization code. +namespace gaia { + +static const char kGaiaOAuth2Url[] = + "https://accounts.google.com/o/oauth2/token"; + +struct OAuthClientInfo { + std::string client_id; + std::string client_secret; +}; + +class GaiaOAuthClient { + public: + class Delegate { + public: + // Invoked on a successful response to the GetTokensFromAuthCode request. + virtual void OnGetTokensResponse(const std::string& refresh_token, + const std::string& access_token, + int expires_in_seconds) = 0; + // Invoked on a successful response to the RefreshToken request. + virtual void OnRefreshTokenResponse(const std::string& access_token, + int expires_in_seconds) = 0; + // Invoked when there is an OAuth error with one of the requests. + virtual void OnOAuthError() = 0; + // Invoked when there is a network error or upon receiving an invalid + // response. This is invoked when the maximum number of retries have been + // exhausted. If max_retries is -1, this is never invoked. + virtual void OnNetworkError(int response_code) = 0; + + protected: + virtual ~Delegate() {} + }; + GaiaOAuthClient(const std::string& gaia_url, + net::URLRequestContextGetter* context_getter); + ~GaiaOAuthClient(); + + // In the below methods, |max_retries| specifies the maximum number of times + // we should retry on a network error in invalid response. This does not + // apply in the case of an OAuth error (i.e. there was something wrong with + // the input arguments). Setting |max_retries| to -1 implies infinite retries. + void GetTokensFromAuthCode(const OAuthClientInfo& oauth_client_info, + const std::string& auth_code, + int max_retries, + Delegate* delegate); + void RefreshToken(const OAuthClientInfo& oauth_client_info, + const std::string& refresh_token, + int max_retries, + Delegate* delegate); + + private: + // The guts of the implementation live in this class. + class Core; + scoped_refptr<Core> core_; + DISALLOW_COPY_AND_ASSIGN(GaiaOAuthClient); +}; +} + +#endif // GOOGLE_APIS_GAIA_GAIA_OAUTH_CLIENT_H_ diff --git a/google_apis/gaia/gaia_oauth_client_unittest.cc b/google_apis/gaia/gaia_oauth_client_unittest.cc new file mode 100644 index 0000000..4991817 --- /dev/null +++ b/google_apis/gaia/gaia_oauth_client_unittest.cc @@ -0,0 +1,243 @@ +// 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. +// +// A complete set of unit tests for GaiaOAuthClient. + +#include <string> + +#include "base/message_loop.h" +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "chrome/test/base/testing_profile.h" +#include "google_apis/gaia/gaia_oauth_client.h" +#include "googleurl/src/gurl.h" +#include "net/base/net_errors.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_status.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; + +namespace { +// Responds as though OAuth returned from the server. +class MockOAuthFetcher : public net::TestURLFetcher { + public: + MockOAuthFetcher(int response_code, + int max_failure_count, + const GURL& url, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) + : net::TestURLFetcher(0, url, d), + max_failure_count_(max_failure_count), + current_failure_count_(0) { + set_url(url); + set_response_code(response_code); + SetResponseString(results); + } + + virtual ~MockOAuthFetcher() { } + + virtual void Start() { + if ((GetResponseCode() != net::HTTP_OK) && (max_failure_count_ != -1) && + (current_failure_count_ == max_failure_count_)) { + set_response_code(net::HTTP_OK); + } + + net::URLRequestStatus::Status code = net::URLRequestStatus::SUCCESS; + if (GetResponseCode() != net::HTTP_OK) { + code = net::URLRequestStatus::FAILED; + current_failure_count_++; + } + set_status(net::URLRequestStatus(code, 0)); + + delegate()->OnURLFetchComplete(this); + } + + private: + int max_failure_count_; + int current_failure_count_; + DISALLOW_COPY_AND_ASSIGN(MockOAuthFetcher); +}; + +class MockOAuthFetcherFactory : public net::URLFetcherFactory, + public net::ScopedURLFetcherFactory { + public: + MockOAuthFetcherFactory() + : net::ScopedURLFetcherFactory(ALLOW_THIS_IN_INITIALIZER_LIST(this)), + response_code_(net::HTTP_OK) { + } + ~MockOAuthFetcherFactory() {} + virtual net::URLFetcher* CreateURLFetcher( + int id, + const GURL& url, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) { + return new MockOAuthFetcher( + response_code_, + max_failure_count_, + url, + results_, + request_type, + d); + } + void set_response_code(int response_code) { + response_code_ = response_code; + } + void set_max_failure_count(int count) { + max_failure_count_ = count; + } + void set_results(const std::string& results) { + results_ = results; + } + private: + int response_code_; + int max_failure_count_; + std::string results_; + DISALLOW_COPY_AND_ASSIGN(MockOAuthFetcherFactory); +}; + +const std::string kTestAccessToken = "1/fFAGRNJru1FTz70BzhT3Zg"; +const std::string kTestRefreshToken = + "1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ"; +const int kTestExpiresIn = 3920; + +const std::string kDummyGetTokensResult = + "{\"access_token\":\"" + kTestAccessToken + "\"," + "\"expires_in\":" + base::IntToString(kTestExpiresIn) + "," + "\"refresh_token\":\"" + kTestRefreshToken + "\"}"; + +const std::string kDummyRefreshTokenResult = + "{\"access_token\":\"" + kTestAccessToken + "\"," + "\"expires_in\":" + base::IntToString(kTestExpiresIn) + "}"; +} + +namespace gaia { + +class GaiaOAuthClientTest : public testing::Test { + public: + GaiaOAuthClientTest() {} + + TestingProfile profile_; + protected: + MessageLoop message_loop_; +}; + +class MockGaiaOAuthClientDelegate : public gaia::GaiaOAuthClient::Delegate { + public: + MockGaiaOAuthClientDelegate() {} + ~MockGaiaOAuthClientDelegate() {} + + MOCK_METHOD3(OnGetTokensResponse, void(const std::string& refresh_token, + const std::string& access_token, int expires_in_seconds)); + MOCK_METHOD2(OnRefreshTokenResponse, void(const std::string& access_token, + int expires_in_seconds)); + MOCK_METHOD0(OnOAuthError, void()); + MOCK_METHOD1(OnNetworkError, void(int response_code)); +}; + +TEST_F(GaiaOAuthClientTest, NetworkFailure) { + int response_code = net::HTTP_INTERNAL_SERVER_ERROR; + + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnNetworkError(response_code)) + .Times(1); + + TestingProfile profile; + + MockOAuthFetcherFactory factory; + factory.set_response_code(response_code); + factory.set_max_failure_count(4); + + OAuthClientInfo client_info; + client_info.client_id = "test_client_id"; + client_info.client_secret = "test_client_secret"; + GaiaOAuthClient auth(kGaiaOAuth2Url, + profile_.GetRequestContext()); + auth.GetTokensFromAuthCode(client_info, "auth_code", 2, &delegate); +} + +TEST_F(GaiaOAuthClientTest, NetworkFailureRecover) { + int response_code = net::HTTP_INTERNAL_SERVER_ERROR; + + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnGetTokensResponse(kTestRefreshToken, kTestAccessToken, + kTestExpiresIn)).Times(1); + + TestingProfile profile; + + MockOAuthFetcherFactory factory; + factory.set_response_code(response_code); + factory.set_max_failure_count(4); + factory.set_results(kDummyGetTokensResult); + + OAuthClientInfo client_info; + client_info.client_id = "test_client_id"; + client_info.client_secret = "test_client_secret"; + GaiaOAuthClient auth(kGaiaOAuth2Url, + profile_.GetRequestContext()); + auth.GetTokensFromAuthCode(client_info, "auth_code", -1, &delegate); +} + +TEST_F(GaiaOAuthClientTest, OAuthFailure) { + int response_code = net::HTTP_BAD_REQUEST; + + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnOAuthError()).Times(1); + + TestingProfile profile; + + MockOAuthFetcherFactory factory; + factory.set_response_code(response_code); + factory.set_max_failure_count(-1); + factory.set_results(kDummyGetTokensResult); + + OAuthClientInfo client_info; + client_info.client_id = "test_client_id"; + client_info.client_secret = "test_client_secret"; + GaiaOAuthClient auth(kGaiaOAuth2Url, + profile_.GetRequestContext()); + auth.GetTokensFromAuthCode(client_info, "auth_code", -1, &delegate); +} + + +TEST_F(GaiaOAuthClientTest, GetTokensSuccess) { + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnGetTokensResponse(kTestRefreshToken, kTestAccessToken, + kTestExpiresIn)).Times(1); + + TestingProfile profile; + + MockOAuthFetcherFactory factory; + factory.set_results(kDummyGetTokensResult); + + OAuthClientInfo client_info; + client_info.client_id = "test_client_id"; + client_info.client_secret = "test_client_secret"; + GaiaOAuthClient auth(kGaiaOAuth2Url, + profile_.GetRequestContext()); + auth.GetTokensFromAuthCode(client_info, "auth_code", -1, &delegate); +} + +TEST_F(GaiaOAuthClientTest, RefreshTokenSuccess) { + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnRefreshTokenResponse(kTestAccessToken, + kTestExpiresIn)).Times(1); + + TestingProfile profile; + + MockOAuthFetcherFactory factory; + factory.set_results(kDummyRefreshTokenResult); + + OAuthClientInfo client_info; + client_info.client_id = "test_client_id"; + client_info.client_secret = "test_client_secret"; + GaiaOAuthClient auth(kGaiaOAuth2Url, + profile_.GetRequestContext()); + auth.GetTokensFromAuthCode(client_info, "auth_code", -1, &delegate); +} +} // namespace gaia diff --git a/google_apis/gaia/gaia_switches.cc b/google_apis/gaia/gaia_switches.cc new file mode 100644 index 0000000..038bc2f --- /dev/null +++ b/google_apis/gaia/gaia_switches.cc @@ -0,0 +1,18 @@ +// 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 "google_apis/gaia/gaia_switches.h" + +namespace switches { + +const char kClientLoginToOAuth2Url[] = "client-login-to-oauth2-url"; +const char kGaiaHost[] = "gaia-host"; +const char kGaiaOAuthHost[] = "gaia-oauth-host"; +const char kGaiaOAuthUrlPath[] = "gaia-oauth-url-path"; +const char kGaiaUrlPath[] = "gaia-url-path"; +const char kOAuth1LoginScope[] = "oauth1-login-scope"; +const char kOAuth2IssueTokenUrl[] = "oauth2-issue-token-url"; +const char kOAuth2TokenUrl[] = "oauth2-token-url"; + +} // namespace switches diff --git a/google_apis/gaia/gaia_switches.h b/google_apis/gaia/gaia_switches.h new file mode 100644 index 0000000..f8ec659 --- /dev/null +++ b/google_apis/gaia/gaia_switches.h @@ -0,0 +1,44 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_GAIA_SWITCHES_H_ +#define GOOGLE_APIS_GAIA_GAIA_SWITCHES_H_ + +namespace switches { + +// Supplies custom client login to OAuth2 URL for testing purposes. +extern const char kClientLoginToOAuth2Url[]; + +// Specifies the backend server used for gaia authentications, like sync or +// policies for example. The https:// prefix and the trailing slash should be +// omitted. The default value is "www.google.com". +extern const char kGaiaHost[]; + +// Specifies the backend server used for OAuth authentication requests. +// The https:// prefix and the trailing slash should be +// omitted. The default value is "www.google.com". +extern const char kGaiaOAuthHost[]; + +// Specifies the path prefix for GAIA OAuth URLs. It should be used +// for testing in cases where authentication path prefix differs from the one +// used in production. +extern const char kGaiaOAuthUrlPath[]; + +// Specifies the path prefix for GAIA authentication URL. It should be used +// for testing in cases where authentication path prefix differs from the one +// used in production. +extern const char kGaiaUrlPath[]; + +// Specifies custom OAuth1 login scope for testing purposes. +extern const char kOAuth1LoginScope[]; + +// Specifies custom OAuth2 issue token URL for testing purposes. +extern const char kOAuth2IssueTokenUrl[]; + +// Specifies custom OAuth2 token URL for testing purposes. +extern const char kOAuth2TokenUrl[]; + +} // namespace switches + +#endif // GOOGLE_APIS_GAIA_GAIA_SWITCHES_H_ diff --git a/google_apis/gaia/gaia_urls.cc b/google_apis/gaia/gaia_urls.cc new file mode 100644 index 0000000..aa5ac38 --- /dev/null +++ b/google_apis/gaia/gaia_urls.cc @@ -0,0 +1,242 @@ +// 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 "google_apis/gaia/gaia_urls.h" + +#include "base/command_line.h" +#include "google_apis/gaia/gaia_switches.h" +#include "google_apis/google_api_keys.h" + +namespace { + +// Gaia service constants +const char kDefaultGaiaBaseUrl[] = "accounts.google.com"; + +// Gaia service constants +const char kDefaultGaiaOAuthBaseUrl[] = "www.google.com"; + +const char kCaptchaUrlPrefixSuffix[] = "/"; +const char kClientLoginUrlSuffix[] = "/ClientLogin"; +const char kServiceLoginUrlSuffix[] = "/ServiceLogin"; +const char kIssueAuthTokenUrlSuffix[] = "/IssueAuthToken"; +const char kGetUserInfoUrlSuffix[] = "/GetUserInfo"; +const char kTokenAuthUrlSuffix[] = "/TokenAuth"; +const char kMergeSessionUrlSuffix[] = "/MergeSession"; + +const char kOAuthGetAccessTokenUrlSuffix[] = "/OAuthGetAccessToken"; +const char kOAuthWrapBridgeUrlSuffix[] = "/OAuthWrapBridge"; +const char kOAuth1LoginUrlSuffix[] = "/OAuthLogin"; +const char kOAuthRevokeTokenUrlSuffix[] = "/AuthSubRevokeToken"; + +// Federated login constants +const char kDefaultFederatedLoginHost[] = "www.google.com"; +const char kDefaultFederatedLoginPath[] = "/accounts"; +const char kGetOAuthTokenUrlSuffix[] = "/o8/GetOAuthToken"; + +const char kClientLoginToOAuth2Url[] = + "https://accounts.google.com/o/oauth2/programmatic_auth"; +const char kOAuth2TokenUrl[] = + "https://accounts.google.com/o/oauth2/token"; +const char kOAuth2IssueTokenUrl[] = + "https://www.googleapis.com/oauth2/v2/IssueToken"; +const char kOAuth1LoginScope[] = + "https://www.google.com/accounts/OAuthLogin"; + +void GetSwitchValueWithDefault(const char* switch_value, + const char* default_value, + std::string* output_value) { + CommandLine* command_line = CommandLine::ForCurrentProcess(); + if (command_line->HasSwitch(switch_value)) { + *output_value = command_line->GetSwitchValueASCII(switch_value); + } else { + *output_value = default_value; + } +} + +} // namespace + +GaiaUrls* GaiaUrls::GetInstance() { + return Singleton<GaiaUrls>::get(); +} + +GaiaUrls::GaiaUrls() { + CommandLine* command_line = CommandLine::ForCurrentProcess(); + std::string host_base; + GetSwitchValueWithDefault(switches::kGaiaHost, kDefaultGaiaBaseUrl, + &host_base); + + captcha_url_prefix_ = "http://" + host_base + kCaptchaUrlPrefixSuffix; + gaia_origin_url_ = "https://" + host_base; + std::string gaia_url_base = gaia_origin_url_; + if (command_line->HasSwitch(switches::kGaiaUrlPath)) { + std::string path = + command_line->GetSwitchValueASCII(switches::kGaiaUrlPath); + if (!path.empty()) { + if (path[0] != '/') + gaia_url_base.append("/"); + + gaia_url_base.append(path); + } + } + + client_login_url_ = gaia_url_base + kClientLoginUrlSuffix; + service_login_url_ = gaia_url_base + kServiceLoginUrlSuffix; + issue_auth_token_url_ = gaia_url_base + kIssueAuthTokenUrlSuffix; + get_user_info_url_ = gaia_url_base + kGetUserInfoUrlSuffix; + token_auth_url_ = gaia_url_base + kTokenAuthUrlSuffix; + merge_session_url_ = gaia_url_base + kMergeSessionUrlSuffix; + + // Federated login is not part of Gaia and has its own endpoints. + std::string oauth_host_base; + GetSwitchValueWithDefault(switches::kGaiaOAuthHost, + kDefaultFederatedLoginHost, + &oauth_host_base); + + std::string gaia_oauth_url_base = "https://"+oauth_host_base; + if (command_line->HasSwitch(switches::kGaiaOAuthUrlPath)) { + std::string path = + command_line->GetSwitchValueASCII(switches::kGaiaOAuthUrlPath); + if (!path.empty()) { + if (path[0] != '/') + gaia_oauth_url_base.append("/"); + + gaia_oauth_url_base.append(path); + } + } else { + gaia_oauth_url_base.append(kDefaultFederatedLoginPath); + } + get_oauth_token_url_ = gaia_oauth_url_base + + kGetOAuthTokenUrlSuffix; + + oauth_get_access_token_url_ = gaia_url_base + + kOAuthGetAccessTokenUrlSuffix; + oauth_wrap_bridge_url_ = gaia_url_base + kOAuthWrapBridgeUrlSuffix; + oauth_revoke_token_url_ = gaia_url_base + kOAuthRevokeTokenUrlSuffix; + oauth1_login_url_ = gaia_url_base + kOAuth1LoginUrlSuffix; + + GetSwitchValueWithDefault(switches::kOAuth1LoginScope, + kOAuth1LoginScope, + &oauth1_login_scope_); + + // TODO(joaodasilva): these aren't configurable for now, but are managed here + // so that users of Gaia URLs don't have to use static constants. + // http://crbug.com/97126 + oauth_user_info_url_ = "https://www.googleapis.com/oauth2/v1/userinfo"; + oauth_wrap_bridge_user_info_scope_ = + "https://www.googleapis.com/auth/userinfo.email"; + client_oauth_url_ = "https://accounts.google.com/ClientOAuth"; + + oauth2_chrome_client_id_ = + google_apis::GetOAuth2ClientID(google_apis::CLIENT_MAIN); + oauth2_chrome_client_secret_ = + google_apis::GetOAuth2ClientSecret(google_apis::CLIENT_MAIN); + + GetSwitchValueWithDefault(switches::kClientLoginToOAuth2Url, + kClientLoginToOAuth2Url, + &client_login_to_oauth2_url_); + GetSwitchValueWithDefault(switches::kOAuth2TokenUrl, + kOAuth2TokenUrl, + &oauth2_token_url_); + GetSwitchValueWithDefault(switches::kOAuth2IssueTokenUrl, + kOAuth2IssueTokenUrl, + &oauth2_issue_token_url_); + + gaia_login_form_realm_ = "https://accounts.google.com/"; +} + +GaiaUrls::~GaiaUrls() { +} + +const std::string& GaiaUrls::captcha_url_prefix() { + return captcha_url_prefix_; +} + +const std::string& GaiaUrls::gaia_origin_url() { + return gaia_origin_url_; +} + +const std::string& GaiaUrls::client_login_url() { + return client_login_url_; +} + +const std::string& GaiaUrls::service_login_url() { + return service_login_url_; +} + +const std::string& GaiaUrls::issue_auth_token_url() { + return issue_auth_token_url_; +} + +const std::string& GaiaUrls::get_user_info_url() { + return get_user_info_url_; +} + +const std::string& GaiaUrls::token_auth_url() { + return token_auth_url_; +} + +const std::string& GaiaUrls::merge_session_url() { + return merge_session_url_; +} + +const std::string& GaiaUrls::get_oauth_token_url() { + return get_oauth_token_url_; +} + +const std::string& GaiaUrls::oauth_get_access_token_url() { + return oauth_get_access_token_url_; +} + +const std::string& GaiaUrls::oauth_wrap_bridge_url() { + return oauth_wrap_bridge_url_; +} + +const std::string& GaiaUrls::oauth_user_info_url() { + return oauth_user_info_url_; +} + +const std::string& GaiaUrls::oauth_revoke_token_url() { + return oauth_revoke_token_url_; +} + +const std::string& GaiaUrls::oauth1_login_url() { + return oauth1_login_url_; +} + +const std::string& GaiaUrls::oauth1_login_scope() { + return oauth1_login_scope_; +} + +const std::string& GaiaUrls::oauth_wrap_bridge_user_info_scope() { + return oauth_wrap_bridge_user_info_scope_; +} + +const std::string& GaiaUrls::client_oauth_url() { + return client_oauth_url_; +} + +const std::string& GaiaUrls::oauth2_chrome_client_id() { + return oauth2_chrome_client_id_; +} + +const std::string& GaiaUrls::oauth2_chrome_client_secret() { + return oauth2_chrome_client_secret_; +} + +const std::string& GaiaUrls::client_login_to_oauth2_url() { + return client_login_to_oauth2_url_; +} + +const std::string& GaiaUrls::oauth2_token_url() { + return oauth2_token_url_; +} + +const std::string& GaiaUrls::oauth2_issue_token_url() { + return oauth2_issue_token_url_; +} + + +const std::string& GaiaUrls::gaia_login_form_realm() { + return gaia_login_form_realm_; +} diff --git a/google_apis/gaia/gaia_urls.h b/google_apis/gaia/gaia_urls.h new file mode 100644 index 0000000..cf7b87c --- /dev/null +++ b/google_apis/gaia/gaia_urls.h @@ -0,0 +1,83 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_GAIA_URLS_H_ +#define GOOGLE_APIS_GAIA_GAIA_URLS_H_ + +#include <string> + +#include "base/memory/singleton.h" + +// A signleton that provides all the URLs that are used for connecting to GAIA. +class GaiaUrls { + public: + static GaiaUrls* GetInstance(); + + // The URLs for different calls in the Google Accounts programmatic login API. + const std::string& captcha_url_prefix(); + + const std::string& gaia_origin_url(); + const std::string& client_login_url(); + const std::string& service_login_url(); + const std::string& issue_auth_token_url(); + const std::string& get_user_info_url(); + const std::string& token_auth_url(); + const std::string& merge_session_url(); + const std::string& get_oauth_token_url(); + const std::string& oauth_get_access_token_url(); + const std::string& oauth_wrap_bridge_url(); + const std::string& oauth_user_info_url(); + const std::string& oauth_revoke_token_url(); + const std::string& oauth1_login_url(); + + const std::string& oauth1_login_scope(); + const std::string& oauth_wrap_bridge_user_info_scope(); + const std::string& client_oauth_url(); + + const std::string& oauth2_chrome_client_id(); + const std::string& oauth2_chrome_client_secret(); + const std::string& client_login_to_oauth2_url(); + const std::string& oauth2_token_url(); + const std::string& oauth2_issue_token_url(); + + const std::string& gaia_login_form_realm(); + + private: + GaiaUrls(); + ~GaiaUrls(); + + friend struct DefaultSingletonTraits<GaiaUrls>; + + std::string captcha_url_prefix_; + + std::string gaia_origin_url_; + std::string client_login_url_; + std::string service_login_url_; + std::string issue_auth_token_url_; + std::string get_user_info_url_; + std::string token_auth_url_; + std::string merge_session_url_; + std::string get_oauth_token_url_; + std::string oauth_get_access_token_url_; + std::string oauth_wrap_bridge_url_; + std::string oauth_user_info_url_; + std::string oauth_revoke_token_url_; + std::string oauth1_login_url_; + + std::string oauth1_login_scope_; + std::string oauth_wrap_bridge_user_info_scope_; + std::string client_oauth_url_; + + std::string oauth2_chrome_client_id_; + std::string oauth2_chrome_client_secret_; + std::string client_login_to_oauth2_url_; + std::string oauth2_token_url_; + std::string oauth2_issue_token_url_; + + std::string gaia_login_form_realm_; + + DISALLOW_COPY_AND_ASSIGN(GaiaUrls); +}; + +#endif // GOOGLE_APIS_GAIA_GAIA_URLS_H_ diff --git a/google_apis/gaia/google_service_auth_error.cc b/google_apis/gaia/google_service_auth_error.cc new file mode 100644 index 0000000..8b115ea --- /dev/null +++ b/google_apis/gaia/google_service_auth_error.cc @@ -0,0 +1,285 @@ +// 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 "google_apis/gaia/google_service_auth_error.h" + +#include <string> + +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/values.h" +#include "net/base/net_errors.h" + +GoogleServiceAuthError::Captcha::Captcha() : image_width(0), image_height(0) { +} + +GoogleServiceAuthError::Captcha::Captcha( + const std::string& token, const GURL& audio, const GURL& img, + const GURL& unlock, int width, int height) + : token(token), audio_url(audio), image_url(img), unlock_url(unlock), + image_width(width), image_height(height) { +} + +GoogleServiceAuthError::Captcha::~Captcha() { +} + +bool GoogleServiceAuthError::Captcha::operator==(const Captcha& b) const { + return (token == b.token && + audio_url == b.audio_url && + image_url == b.image_url && + unlock_url == b.unlock_url && + image_width == b.image_width && + image_height == b.image_height); +} + + +GoogleServiceAuthError::SecondFactor::SecondFactor() : field_length(0) { +} + +GoogleServiceAuthError::SecondFactor::SecondFactor( + const std::string& token, const std::string& prompt, + const std::string& alternate, int length) + : token(token), prompt_text(prompt), alternate_text(alternate), + field_length(length) { +} + +GoogleServiceAuthError::SecondFactor::~SecondFactor() { +} + +bool GoogleServiceAuthError::SecondFactor::operator==( + const SecondFactor& b) const { + return (token == b.token && + prompt_text == b.prompt_text && + alternate_text == b.alternate_text && + field_length == b.field_length); +} + + +bool GoogleServiceAuthError::operator==( + const GoogleServiceAuthError& b) const { + return (state_ == b.state_ && + network_error_ == b.network_error_ && + captcha_ == b.captcha_ && + second_factor_ == b.second_factor_); +} + +GoogleServiceAuthError::GoogleServiceAuthError(State s) + : state_(s), + network_error_(0) { + // If the caller has no idea, then we just set it to a generic failure. + if (s == CONNECTION_FAILED) { + network_error_ = net::ERR_FAILED; + } +} + +GoogleServiceAuthError::GoogleServiceAuthError(const std::string& error_message) + : state_(INVALID_GAIA_CREDENTIALS), + network_error_(0), + error_message_(error_message) { +} + +// static +GoogleServiceAuthError + GoogleServiceAuthError::FromConnectionError(int error) { + return GoogleServiceAuthError(CONNECTION_FAILED, error); +} + +// static +GoogleServiceAuthError GoogleServiceAuthError::FromClientLoginCaptchaChallenge( + const std::string& captcha_token, + const GURL& captcha_image_url, + const GURL& captcha_unlock_url) { + return GoogleServiceAuthError(CAPTCHA_REQUIRED, captcha_token, GURL(), + captcha_image_url, captcha_unlock_url, 0, 0); +} + +// static +GoogleServiceAuthError GoogleServiceAuthError::FromCaptchaChallenge( + const std::string& captcha_token, + const GURL& captcha_audio_url, + const GURL& captcha_image_url, + int image_width, + int image_height) { + return GoogleServiceAuthError(CAPTCHA_REQUIRED, captcha_token, + captcha_audio_url, captcha_image_url, + GURL(), image_width, image_height); +} + +// static +GoogleServiceAuthError GoogleServiceAuthError::FromSecondFactorChallenge( + const std::string& captcha_token, + const std::string& prompt_text, + const std::string& alternate_text, + int field_length) { + return GoogleServiceAuthError(TWO_FACTOR, captcha_token, prompt_text, + alternate_text, field_length); +} + +// static +GoogleServiceAuthError GoogleServiceAuthError::FromClientOAuthError( + const std::string& data) { + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + return GoogleServiceAuthError(CONNECTION_FAILED, 0); + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + + std::string cause; + if (!dict->GetStringWithoutPathExpansion("cause", &cause)) + return GoogleServiceAuthError(CONNECTION_FAILED, 0); + + // The explanation field is optional. + std::string explanation; + if (!dict->GetStringWithoutPathExpansion("explanation", &explanation)) + explanation.clear(); + + return GoogleServiceAuthError(explanation); +} + +GoogleServiceAuthError GoogleServiceAuthError::None() { + return GoogleServiceAuthError(NONE); +} + +GoogleServiceAuthError::State GoogleServiceAuthError::state() const { + return state_; +} + +const GoogleServiceAuthError::Captcha& GoogleServiceAuthError::captcha() const { + return captcha_; +} + +const GoogleServiceAuthError::SecondFactor& +GoogleServiceAuthError::second_factor() const { + return second_factor_; +} + +int GoogleServiceAuthError::network_error() const { + return network_error_; +} + +const std::string& GoogleServiceAuthError::token() const { + switch (state_) { + case CAPTCHA_REQUIRED: + return captcha_.token; + break; + case TWO_FACTOR: + return second_factor_.token; + break; + default: + NOTREACHED(); + } + return EmptyString(); +} + +const std::string& GoogleServiceAuthError::error_message() const { + return error_message_; +} + +DictionaryValue* GoogleServiceAuthError::ToValue() const { + DictionaryValue* value = new DictionaryValue(); + std::string state_str; + switch (state_) { +#define STATE_CASE(x) case x: state_str = #x; break + STATE_CASE(NONE); + STATE_CASE(INVALID_GAIA_CREDENTIALS); + STATE_CASE(USER_NOT_SIGNED_UP); + STATE_CASE(CONNECTION_FAILED); + STATE_CASE(CAPTCHA_REQUIRED); + STATE_CASE(ACCOUNT_DELETED); + STATE_CASE(ACCOUNT_DISABLED); + STATE_CASE(SERVICE_UNAVAILABLE); + STATE_CASE(TWO_FACTOR); + STATE_CASE(REQUEST_CANCELED); + STATE_CASE(HOSTED_NOT_ALLOWED); +#undef STATE_CASE + default: + NOTREACHED(); + break; + } + value->SetString("state", state_str); + if (state_ == CAPTCHA_REQUIRED) { + DictionaryValue* captcha_value = new DictionaryValue(); + value->Set("captcha", captcha_value); + captcha_value->SetString("token", captcha_.token); + captcha_value->SetString("audioUrl", captcha_.audio_url.spec()); + captcha_value->SetString("imageUrl", captcha_.image_url.spec()); + captcha_value->SetString("unlockUrl", captcha_.unlock_url.spec()); + captcha_value->SetInteger("imageWidth", captcha_.image_width); + captcha_value->SetInteger("imageHeight", captcha_.image_height); + } else if (state_ == CONNECTION_FAILED) { + value->SetString("networkError", net::ErrorToString(network_error_)); + } else if (state_ == TWO_FACTOR) { + DictionaryValue* two_factor_value = new DictionaryValue(); + value->Set("two_factor", two_factor_value); + two_factor_value->SetString("token", second_factor_.token); + two_factor_value->SetString("promptText", second_factor_.prompt_text); + two_factor_value->SetString("alternateText", second_factor_.alternate_text); + two_factor_value->SetInteger("fieldLength", second_factor_.field_length); + } + return value; +} + +std::string GoogleServiceAuthError::ToString() const { + switch (state_) { + case NONE: + return ""; + case INVALID_GAIA_CREDENTIALS: + return "Invalid credentials."; + case USER_NOT_SIGNED_UP: + return "Not authorized."; + case CONNECTION_FAILED: + return base::StringPrintf("Connection failed (%d).", network_error_); + case CAPTCHA_REQUIRED: + return base::StringPrintf("CAPTCHA required (%s).", + captcha_.token.c_str()); + case ACCOUNT_DELETED: + return "Account deleted."; + case ACCOUNT_DISABLED: + return "Account disabled."; + case SERVICE_UNAVAILABLE: + return "Service unavailable; try again later."; + case TWO_FACTOR: + return base::StringPrintf("2-step verification required (%s).", + second_factor_.token.c_str()); + case REQUEST_CANCELED: + return "Request canceled."; + case HOSTED_NOT_ALLOWED: + return "Google account required."; + default: + NOTREACHED(); + return std::string(); + } +} + +GoogleServiceAuthError::GoogleServiceAuthError(State s, int error) + : state_(s), + network_error_(error) { +} + +GoogleServiceAuthError::GoogleServiceAuthError( + State s, + const std::string& captcha_token, + const GURL& captcha_audio_url, + const GURL& captcha_image_url, + const GURL& captcha_unlock_url, + int image_width, + int image_height) + : state_(s), + captcha_(captcha_token, captcha_audio_url, captcha_image_url, + captcha_unlock_url, image_width, image_height), + network_error_(0) { +} + +GoogleServiceAuthError::GoogleServiceAuthError( + State s, + const std::string& captcha_token, + const std::string& prompt_text, + const std::string& alternate_text, + int field_length) + : state_(s), + second_factor_(captcha_token, prompt_text, alternate_text, field_length), + network_error_(0) { +} diff --git a/google_apis/gaia/google_service_auth_error.h b/google_apis/gaia/google_service_auth_error.h new file mode 100644 index 0000000..ea434bd --- /dev/null +++ b/google_apis/gaia/google_service_auth_error.h @@ -0,0 +1,209 @@ +// 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. + +// A GoogleServiceAuthError is immutable, plain old data representing an +// error from an attempt to authenticate with a Google service. +// It could be from Google Accounts itself, or any service using Google +// Accounts (e.g expired credentials). It may contain additional data such as +// captcha or OTP challenges. + +// A GoogleServiceAuthError without additional data is just a State, defined +// below. A case could be made to have this relation implicit, to allow raising +// error events concisely by doing OnAuthError(GoogleServiceAuthError::NONE), +// for example. But the truth is this class is ever so slightly more than a +// transparent wrapper around 'State' due to additional Captcha data +// (e.g consider operator=), and this would violate the style guide. Thus, +// you must explicitly use the constructor when all you have is a State. +// The good news is the implementation nests the enum inside a class, so you +// may forward declare and typedef GoogleServiceAuthError to something shorter +// in the comfort of your own translation unit. + +#ifndef GOOGLE_APIS_GAIA_GOOGLE_SERVICE_AUTH_ERROR_H_ +#define GOOGLE_APIS_GAIA_GOOGLE_SERVICE_AUTH_ERROR_H_ + +#include <string> + +#include "googleurl/src/gurl.h" + +namespace base { +class DictionaryValue; +} + +class GoogleServiceAuthError { + public: + // + // These enumerations are referenced by integer value in HTML login code. + // Do not change the numeric values. + // + enum State { + // The user is authenticated. + NONE = 0, + + // The credentials supplied to GAIA were either invalid, or the locally + // cached credentials have expired. + INVALID_GAIA_CREDENTIALS = 1, + + // The GAIA user is not authorized to use the service. + USER_NOT_SIGNED_UP = 2, + + // Could not connect to server to verify credentials. This could be in + // response to either failure to connect to GAIA or failure to connect to + // the service needing GAIA tokens during authentication. + CONNECTION_FAILED = 3, + + // The user needs to satisfy a CAPTCHA challenge to unlock their account. + // If no other information is available, this can be resolved by visiting + // https://accounts.google.com/DisplayUnlockCaptcha. Otherwise, captcha() + // will provide details about the associated challenge. + CAPTCHA_REQUIRED = 4, + + // The user account has been deleted. + ACCOUNT_DELETED = 5, + + // The user account has been disabled. + ACCOUNT_DISABLED = 6, + + // The service is not available; try again later. + SERVICE_UNAVAILABLE = 7, + + // The password is valid but we need two factor to get a token. + TWO_FACTOR = 8, + + // The requestor of the authentication step cancelled the request + // prior to completion. + REQUEST_CANCELED = 9, + + // The user has provided a HOSTED account, when this service requires + // a GOOGLE account. + HOSTED_NOT_ALLOWED = 10, + }; + + // Additional data for CAPTCHA_REQUIRED errors. + struct Captcha { + Captcha(); + Captcha(const std::string& token, + const GURL& audio, + const GURL& img, + const GURL& unlock, + int width, + int height); + ~Captcha(); + // For test only. + bool operator==(const Captcha &b) const; + + std::string token; // Globally identifies the specific CAPTCHA challenge. + GURL audio_url; // The CAPTCHA audio to use instead of image. + GURL image_url; // The CAPTCHA image to show the user. + GURL unlock_url; // Pretty unlock page containing above captcha. + int image_width; // Width of captcha image. + int image_height; // Height of capture image. + }; + + // Additional data for TWO_FACTOR errors. + struct SecondFactor { + SecondFactor(); + SecondFactor(const std::string& token, + const std::string& prompt, + const std::string& alternate, + int length); + ~SecondFactor(); + // For test only. + bool operator==(const SecondFactor &b) const; + + // Globally identifies the specific second-factor challenge. + std::string token; + // Localised prompt text, eg “Enter the verification code sent to your + // phone number ending in XXX”. + std::string prompt_text; + // Localized text describing an alternate option, eg “Get a verification + // code in a text message”. + std::string alternate_text; + // Character length for the challenge field. + int field_length; + }; + + // For test only. + bool operator==(const GoogleServiceAuthError &b) const; + + // Construct a GoogleServiceAuthError from a State with no additional data. + explicit GoogleServiceAuthError(State s); + + // Construct a GoogleServiceAuthError from a network error. + // It will be created with CONNECTION_FAILED set. + static GoogleServiceAuthError FromConnectionError(int error); + + // Construct a CAPTCHA_REQUIRED error with CAPTCHA challenge data from the + // the ClientLogin endpoint. + // TODO(rogerta): once ClientLogin is no longer used, may be able to get + // rid of this function. + static GoogleServiceAuthError FromClientLoginCaptchaChallenge( + const std::string& captcha_token, + const GURL& captcha_image_url, + const GURL& captcha_unlock_url); + + // Construct a CAPTCHA_REQUIRED error with CAPTCHA challenge data from the + // ClientOAuth endpoint. + static GoogleServiceAuthError FromCaptchaChallenge( + const std::string& captcha_token, + const GURL& captcha_audio_url, + const GURL& captcha_image_url, + int image_width, + int image_height); + + // Construct a TWO_FACTOR error with second-factor challenge data. + static GoogleServiceAuthError FromSecondFactorChallenge( + const std::string& captcha_token, + const std::string& prompt_text, + const std::string& alternate_text, + int field_length); + + // Construct an INVALID_GAIA_CREDENTIALS error from a ClientOAuth response. + // |data| is the JSON response from the server explaning the error. + static GoogleServiceAuthError FromClientOAuthError(const std::string& data); + + // Provided for convenience for clients needing to reset an instance to NONE. + // (avoids err_ = GoogleServiceAuthError(GoogleServiceAuthError::NONE), due + // to explicit class and State enum relation. Note: shouldn't be inlined! + static GoogleServiceAuthError None(); + + // The error information. + State state() const; + const Captcha& captcha() const; + const SecondFactor& second_factor() const; + int network_error() const; + const std::string& token() const; + const std::string& error_message() const; + + // Returns info about this object in a dictionary. Caller takes + // ownership of returned dictionary. + base::DictionaryValue* ToValue() const; + + // Returns a message describing the error. + std::string ToString() const; + + private: + GoogleServiceAuthError(State s, int error); + + explicit GoogleServiceAuthError(const std::string& error_message); + + GoogleServiceAuthError(State s, const std::string& captcha_token, + const GURL& captcha_audio_url, + const GURL& captcha_image_url, + const GURL& captcha_unlock_url, + int image_width, + int image_height); + + GoogleServiceAuthError(State s, const std::string& captcha_token, + const std::string& prompt_text, + const std::string& alternate_text, + int field_length); + + State state_; + Captcha captcha_; + SecondFactor second_factor_; + int network_error_; + std::string error_message_; +}; + +#endif // GOOGLE_APIS_GAIA_GOOGLE_SERVICE_AUTH_ERROR_H_ diff --git a/google_apis/gaia/google_service_auth_error_unittest.cc b/google_apis/gaia/google_service_auth_error_unittest.cc new file mode 100644 index 0000000..4b83fa6 --- /dev/null +++ b/google_apis/gaia/google_service_auth_error_unittest.cc @@ -0,0 +1,124 @@ +// 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 "google_apis/gaia/google_service_auth_error.h" + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/test/values_test_util.h" +#include "base/values.h" +#include "net/base/net_errors.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using base::ExpectDictStringValue; + +class GoogleServiceAuthErrorTest : public testing::Test {}; + +void TestSimpleState(GoogleServiceAuthError::State state) { + GoogleServiceAuthError error(state); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(1u, value->size()); + std::string state_str; + EXPECT_TRUE(value->GetString("state", &state_str)); + EXPECT_FALSE(state_str.empty()); + EXPECT_NE("CONNECTION_FAILED", state_str); + EXPECT_NE("CAPTCHA_REQUIRED", state_str); +} + +TEST_F(GoogleServiceAuthErrorTest, SimpleToValue) { + for (int i = GoogleServiceAuthError::NONE; + i <= GoogleServiceAuthError::USER_NOT_SIGNED_UP; ++i) { + TestSimpleState(static_cast<GoogleServiceAuthError::State>(i)); + } +} + +TEST_F(GoogleServiceAuthErrorTest, None) { + GoogleServiceAuthError error(GoogleServiceAuthError::None()); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(1u, value->size()); + ExpectDictStringValue("NONE", *value, "state"); +} + +TEST_F(GoogleServiceAuthErrorTest, ConnectionFailed) { + GoogleServiceAuthError error( + GoogleServiceAuthError::FromConnectionError(net::OK)); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(2u, value->size()); + ExpectDictStringValue("CONNECTION_FAILED", *value, "state"); + ExpectDictStringValue("net::OK", *value, "networkError"); +} + +TEST_F(GoogleServiceAuthErrorTest, CaptchaChallenge) { + GoogleServiceAuthError error( + GoogleServiceAuthError::FromClientLoginCaptchaChallenge( + "captcha_token", GURL("http://www.google.com"), + GURL("http://www.bing.com"))); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(2u, value->size()); + ExpectDictStringValue("CAPTCHA_REQUIRED", *value, "state"); + DictionaryValue* captcha_value = NULL; + EXPECT_TRUE(value->GetDictionary("captcha", &captcha_value)); + ASSERT_TRUE(captcha_value); + ExpectDictStringValue("captcha_token", *captcha_value, "token"); + ExpectDictStringValue("", *captcha_value, "audioUrl"); + ExpectDictStringValue("http://www.google.com/", *captcha_value, "imageUrl"); + ExpectDictStringValue("http://www.bing.com/", *captcha_value, "unlockUrl"); + ExpectDictIntegerValue(0, *captcha_value, "imageWidth"); + ExpectDictIntegerValue(0, *captcha_value, "imageHeight"); +} + +TEST_F(GoogleServiceAuthErrorTest, CaptchaChallenge2) { + GoogleServiceAuthError error( + GoogleServiceAuthError::FromCaptchaChallenge( + "captcha_token", GURL("http://www.audio.com"), + GURL("http://www.image.com"), 320, 200)); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(2u, value->size()); + ExpectDictStringValue("CAPTCHA_REQUIRED", *value, "state"); + DictionaryValue* captcha_value = NULL; + EXPECT_TRUE(value->GetDictionary("captcha", &captcha_value)); + ASSERT_TRUE(captcha_value); + ExpectDictStringValue("captcha_token", *captcha_value, "token"); + ExpectDictStringValue("http://www.audio.com/", *captcha_value, "audioUrl"); + ExpectDictStringValue("http://www.image.com/", *captcha_value, "imageUrl"); + ExpectDictIntegerValue(320, *captcha_value, "imageWidth"); + ExpectDictIntegerValue(200, *captcha_value, "imageHeight"); +} + +TEST_F(GoogleServiceAuthErrorTest, TwoFactorChallenge) { + GoogleServiceAuthError error( + GoogleServiceAuthError::FromSecondFactorChallenge( + "two_factor_token", "prompt_text", "alternate_text", 10)); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(2u, value->size()); + ExpectDictStringValue("TWO_FACTOR", *value, "state"); + DictionaryValue* two_factor_value = NULL; + EXPECT_TRUE(value->GetDictionary("two_factor", &two_factor_value)); + ASSERT_TRUE(two_factor_value); + ExpectDictStringValue("two_factor_token", *two_factor_value, "token"); + ExpectDictStringValue("prompt_text", *two_factor_value, "promptText"); + ExpectDictStringValue("alternate_text", *two_factor_value, "alternateText"); + ExpectDictIntegerValue(10, *two_factor_value, "fieldLength"); +} + +TEST_F(GoogleServiceAuthErrorTest, ClientOAuthError) { + // Test that a malformed/incomplete ClientOAuth response generates + // a connection problem error. + GoogleServiceAuthError error1( + GoogleServiceAuthError::FromClientOAuthError("{}")); + EXPECT_EQ(GoogleServiceAuthError::CONNECTION_FAILED, error1.state()); + + // Test that a well formed ClientOAuth response generates an invalid + // credentials error with the given error message. + GoogleServiceAuthError error2( + GoogleServiceAuthError::FromClientOAuthError( + "{\"cause\":\"foo\",\"explanation\":\"error_message\"}")); + EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error2.state()); + EXPECT_EQ("error_message", error2.error_message()); +} + +} // namespace diff --git a/google_apis/gaia/mock_url_fetcher_factory.h b/google_apis/gaia/mock_url_fetcher_factory.h new file mode 100644 index 0000000..d765219 --- /dev/null +++ b/google_apis/gaia/mock_url_fetcher_factory.h @@ -0,0 +1,70 @@ +// 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. +// +// A collection of classes that are useful when testing things that use a +// GaiaAuthFetcher. + +#ifndef GOOGLE_APIS_GAIA_MOCK_URL_FETCHER_FACTORY_H_ +#define GOOGLE_APIS_GAIA_MOCK_URL_FETCHER_FACTORY_H_ + +#include <string> + +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_status.h" + +// Responds as though ClientLogin returned from the server. +class MockFetcher : public net::TestURLFetcher { + public: + MockFetcher(bool success, + const GURL& url, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d); + + MockFetcher(const GURL& url, + const net::URLRequestStatus& status, + int response_code, + const net::ResponseCookies& cookies, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d); + + virtual ~MockFetcher(); + + virtual void Start() OVERRIDE; + + private: + DISALLOW_COPY_AND_ASSIGN(MockFetcher); +}; + +template<typename T> +class MockURLFetcherFactory : public net::URLFetcherFactory, + public net::ScopedURLFetcherFactory { + public: + MockURLFetcherFactory() + : net::ScopedURLFetcherFactory(ALLOW_THIS_IN_INITIALIZER_LIST(this)), + success_(true) { + } + ~MockURLFetcherFactory() {} + net::URLFetcher* CreateURLFetcher( + int id, + const GURL& url, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) OVERRIDE { + return new T(success_, url, results_, request_type, d); + } + void set_success(bool success) { + success_ = success; + } + void set_results(const std::string& results) { + results_ = results; + } + private: + bool success_; + std::string results_; + DISALLOW_COPY_AND_ASSIGN(MockURLFetcherFactory); +}; + +#endif // GOOGLE_APIS_GAIA_MOCK_URL_FETCHER_FACTORY_H_ diff --git a/google_apis/gaia/oauth2_access_token_consumer.h b/google_apis/gaia/oauth2_access_token_consumer.h new file mode 100644 index 0000000..65d32e8 --- /dev/null +++ b/google_apis/gaia/oauth2_access_token_consumer.h @@ -0,0 +1,31 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ + +#include <string> + +class GoogleServiceAuthError; + +namespace base { +class Time; +} + +// An interface that defines the callbacks for consumers to which +// OAuth2AccessTokenFetcher can return results. +class OAuth2AccessTokenConsumer { + public: + // Success callback. |access_token| will contain a valid OAuth2 access token. + // |expiration_time| is the date until which the token can be used. This + // value has a built-in safety margin, so it can be used as-is. + virtual void OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time) {} + virtual void OnGetTokenFailure(const GoogleServiceAuthError& error) {} + + protected: + virtual ~OAuth2AccessTokenConsumer() {} +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ diff --git a/google_apis/gaia/oauth2_access_token_fetcher.cc b/google_apis/gaia/oauth2_access_token_fetcher.cc new file mode 100644 index 0000000..679891c --- /dev/null +++ b/google_apis/gaia/oauth2_access_token_fetcher.cc @@ -0,0 +1,218 @@ +// 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 "google_apis/gaia/oauth2_access_token_fetcher.h" + +#include <algorithm> +#include <string> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/time.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using net::ResponseCookies; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { +static const char kGetAccessTokenBodyFormat[] = + "client_id=%s&" + "client_secret=%s&" + "grant_type=refresh_token&" + "refresh_token=%s"; + +static const char kGetAccessTokenBodyWithScopeFormat[] = + "client_id=%s&" + "client_secret=%s&" + "grant_type=refresh_token&" + "refresh_token=%s&" + "scope=%s"; + +static const char kAccessTokenKey[] = "access_token"; +static const char kExpiresInKey[] = "expires_in"; + +static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) { + CHECK(!status.is_success()); + if (status.status() == URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } +} + +static URLFetcher* CreateFetcher(URLRequestContextGetter* getter, + const GURL& url, + const std::string& body, + URLFetcherDelegate* delegate) { + bool empty_body = body.empty(); + URLFetcher* result = net::URLFetcher::Create( + 0, url, + empty_body ? URLFetcher::GET : URLFetcher::POST, + delegate); + + result->SetRequestContext(getter); + result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + + if (!empty_body) + result->SetUploadData("application/x-www-form-urlencoded", body); + + return result; +} +} // namespace + +OAuth2AccessTokenFetcher::OAuth2AccessTokenFetcher( + OAuth2AccessTokenConsumer* consumer, + URLRequestContextGetter* getter) + : consumer_(consumer), + getter_(getter), + state_(INITIAL) { } + +OAuth2AccessTokenFetcher::~OAuth2AccessTokenFetcher() { } + +void OAuth2AccessTokenFetcher::CancelRequest() { + fetcher_.reset(); +} + +void OAuth2AccessTokenFetcher::Start(const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes) { + client_id_ = client_id; + client_secret_ = client_secret; + refresh_token_ = refresh_token; + scopes_ = scopes; + StartGetAccessToken(); +} + +void OAuth2AccessTokenFetcher::StartGetAccessToken() { + CHECK_EQ(INITIAL, state_); + state_ = GET_ACCESS_TOKEN_STARTED; + fetcher_.reset(CreateFetcher( + getter_, + MakeGetAccessTokenUrl(), + MakeGetAccessTokenBody( + client_id_, client_secret_, refresh_token_, scopes_), + this)); + fetcher_->Start(); // OnURLFetchComplete will be called. +} + +void OAuth2AccessTokenFetcher::EndGetAccessToken( + const net::URLFetcher* source) { + CHECK_EQ(GET_ACCESS_TOKEN_STARTED, state_); + state_ = GET_ACCESS_TOKEN_DONE; + + URLRequestStatus status = source->GetStatus(); + if (!status.is_success()) { + OnGetTokenFailure(CreateAuthError(status)); + return; + } + + if (source->GetResponseCode() != net::HTTP_OK) { + OnGetTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + return; + } + + // The request was successfully fetched and it returned OK. + // Parse out the access token and the expiration time. + std::string access_token; + int expires_in; + if (!ParseGetAccessTokenResponse(source, &access_token, &expires_in)) { + DLOG(WARNING) << "Response doesn't match expected format"; + OnGetTokenFailure( + GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE)); + return; + } + // The token will expire in |expires_in| seconds. Take a 10% error margin to + // prevent reusing a token too close to its expiration date. + OnGetTokenSuccess( + access_token, + base::Time::Now() + base::TimeDelta::FromSeconds(9 * expires_in / 10)); +} + +void OAuth2AccessTokenFetcher::OnGetTokenSuccess( + const std::string& access_token, + const base::Time& expiration_time) { + consumer_->OnGetTokenSuccess(access_token, expiration_time); +} + +void OAuth2AccessTokenFetcher::OnGetTokenFailure( + const GoogleServiceAuthError& error) { + state_ = ERROR_STATE; + consumer_->OnGetTokenFailure(error); +} + +void OAuth2AccessTokenFetcher::OnURLFetchComplete( + const net::URLFetcher* source) { + CHECK(source); + CHECK(state_ == GET_ACCESS_TOKEN_STARTED); + EndGetAccessToken(source); +} + +// static +GURL OAuth2AccessTokenFetcher::MakeGetAccessTokenUrl() { + return GURL(GaiaUrls::GetInstance()->oauth2_token_url()); +} + +// static +std::string OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( + const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes) { + std::string enc_client_id = net::EscapeUrlEncodedData(client_id, true); + std::string enc_client_secret = + net::EscapeUrlEncodedData(client_secret, true); + std::string enc_refresh_token = + net::EscapeUrlEncodedData(refresh_token, true); + if (scopes.empty()) { + return StringPrintf( + kGetAccessTokenBodyFormat, + enc_client_id.c_str(), + enc_client_secret.c_str(), + enc_refresh_token.c_str()); + } else { + std::string scopes_string = JoinString(scopes, ' '); + return StringPrintf( + kGetAccessTokenBodyWithScopeFormat, + enc_client_id.c_str(), + enc_client_secret.c_str(), + enc_refresh_token.c_str(), + net::EscapeUrlEncodedData(scopes_string, true).c_str()); + } +} + +// static +bool OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + const net::URLFetcher* source, + std::string* access_token, + int* expires_in) { + CHECK(source); + CHECK(access_token); + std::string data; + source->GetResponseAsString(&data); + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + return false; + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + return dict->GetString(kAccessTokenKey, access_token) && + dict->GetInteger(kExpiresInKey, expires_in); +} diff --git a/google_apis/gaia/oauth2_access_token_fetcher.h b/google_apis/gaia/oauth2_access_token_fetcher.h new file mode 100644 index 0000000..24f4d85 --- /dev/null +++ b/google_apis/gaia/oauth2_access_token_fetcher.h @@ -0,0 +1,118 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ + +#include <string> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/oauth2_access_token_consumer.h" +#include "googleurl/src/gurl.h" +#include "net/url_request/url_fetcher_delegate.h" + +class OAuth2AccessTokenFetcherTest; + +namespace base { +class Time; +} + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +class URLRequestStatus; +} + +// Abstracts the details to get OAuth2 access token token from +// OAuth2 refresh token. +// See "Using the Refresh Token" section in: +// http://code.google.com/apis/accounts/docs/OAuth2WebServer.html +// +// This class should be used on a single thread, but it can be whichever thread +// that you like. +// Also, do not reuse the same instance. Once Start() is called, the instance +// should not be reused. +// +// Usage: +// * Create an instance with a consumer. +// * Call Start() +// * The consumer passed in the constructor will be called on the same +// thread Start was called with the results. +// +// This class can handle one request at a time. To parallelize requests, +// create multiple instances. +class OAuth2AccessTokenFetcher : public net::URLFetcherDelegate { + public: + OAuth2AccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, + net::URLRequestContextGetter* getter); + virtual ~OAuth2AccessTokenFetcher(); + + // Starts the flow with the given parameters. + // |scopes| can be empty. If it is empty then the access token will have the + // same scope as the refresh token. If not empty, then access token will have + // the scopes specified. In this case, the access token will successfully be + // generated only if refresh token has login scope of a list of scopes that is + // a super-set of the specified scopes. + virtual void Start(const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes); + + void CancelRequest(); + + // Implementation of net::URLFetcherDelegate + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + enum State { + INITIAL, + GET_ACCESS_TOKEN_STARTED, + GET_ACCESS_TOKEN_DONE, + ERROR_STATE, + }; + + // Helper methods for the flow. + void StartGetAccessToken(); + void EndGetAccessToken(const net::URLFetcher* source); + + // Helper mehtods for reporting back results. + void OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time); + void OnGetTokenFailure(const GoogleServiceAuthError& error); + + // Other helpers. + static GURL MakeGetAccessTokenUrl(); + static std::string MakeGetAccessTokenBody( + const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes); + static bool ParseGetAccessTokenResponse(const net::URLFetcher* source, + std::string* access_token, + int* expires_in); + + // State that is set during construction. + OAuth2AccessTokenConsumer* const consumer_; + net::URLRequestContextGetter* const getter_; + State state_; + + // While a fetch is in progress. + scoped_ptr<net::URLFetcher> fetcher_; + std::string client_id_; + std::string client_secret_; + std::string refresh_token_; + std::vector<std::string> scopes_; + + friend class OAuth2AccessTokenFetcherTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherTest, + ParseGetAccessTokenResponse); + FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherTest, + MakeGetAccessTokenBody); + + DISALLOW_COPY_AND_ASSIGN(OAuth2AccessTokenFetcher); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ diff --git a/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc b/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc new file mode 100644 index 0000000..feb8020 --- /dev/null +++ b/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc @@ -0,0 +1,235 @@ +// 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. +// +// A complete set of unit tests for OAuth2AccessTokenFetcher. + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/test_browser_thread.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 "googleurl/src/gurl.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_fetcher_factory.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_status.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using content::BrowserThread; +using net::ResponseCookies; +using net::ScopedURLFetcherFactory; +using net::TestURLFetcher; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLFetcherFactory; +using net::URLRequestStatus; +using testing::_; +using testing::Return; + +namespace { + +typedef std::vector<std::string> ScopeList; + +static const char kValidTokenResponse[] = + "{" + " \"access_token\": \"at1\"," + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + "}"; +static const char kTokenResponseNoAccessToken[] = + "{" + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + "}"; + +class MockUrlFetcherFactory : public ScopedURLFetcherFactory, + public URLFetcherFactory { +public: + MockUrlFetcherFactory() + : ScopedURLFetcherFactory(ALLOW_THIS_IN_INITIALIZER_LIST(this)) { + } + virtual ~MockUrlFetcherFactory() {} + + MOCK_METHOD4( + CreateURLFetcher, + URLFetcher* (int id, + const GURL& url, + URLFetcher::RequestType request_type, + URLFetcherDelegate* d)); +}; + +class MockOAuth2AccessTokenConsumer : public OAuth2AccessTokenConsumer { + public: + MockOAuth2AccessTokenConsumer() {} + ~MockOAuth2AccessTokenConsumer() {} + + MOCK_METHOD2(OnGetTokenSuccess, void(const std::string& access_token, + const base::Time& expiration_time)); + MOCK_METHOD1(OnGetTokenFailure, + void(const GoogleServiceAuthError& error)); +}; + +} // namespace + +class OAuth2AccessTokenFetcherTest : public testing::Test { + public: + OAuth2AccessTokenFetcherTest() + : ui_thread_(BrowserThread::UI, &message_loop_), + fetcher_(&consumer_, profile_.GetRequestContext()) { + } + + virtual ~OAuth2AccessTokenFetcherTest() { } + + virtual TestURLFetcher* SetupGetAccessToken( + bool fetch_succeeds, int response_code, const std::string& body) { + GURL url(GaiaUrls::GetInstance()->oauth2_token_url()); + TestURLFetcher* url_fetcher = new TestURLFetcher(0, url, &fetcher_); + URLRequestStatus::Status status = + fetch_succeeds ? URLRequestStatus::SUCCESS : URLRequestStatus::FAILED; + url_fetcher->set_status(URLRequestStatus(status, 0)); + + if (response_code != 0) + url_fetcher->set_response_code(response_code); + + if (!body.empty()) + url_fetcher->SetResponseString(body); + + EXPECT_CALL(factory_, CreateURLFetcher(_, url, _, _)) + .WillOnce(Return(url_fetcher)); + return url_fetcher; + } + + protected: + MessageLoop message_loop_; + content::TestBrowserThread ui_thread_; + MockUrlFetcherFactory factory_; + MockOAuth2AccessTokenConsumer consumer_; + TestingProfile profile_; + OAuth2AccessTokenFetcher fetcher_; +}; + +// These four tests time out, see http://crbug.com/113446. +TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_GetAccessTokenRequestFailure) { + TestURLFetcher* url_fetcher = SetupGetAccessToken(false, 0, ""); + EXPECT_CALL(consumer_, OnGetTokenFailure(_)).Times(1); + fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, + DISABLED_GetAccessTokenResponseCodeFailure) { + TestURLFetcher* url_fetcher = + SetupGetAccessToken(true, net::HTTP_FORBIDDEN, ""); + EXPECT_CALL(consumer_, OnGetTokenFailure(_)).Times(1); + fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_Success) { + TestURLFetcher* url_fetcher = SetupGetAccessToken( + true, net::HTTP_OK, kValidTokenResponse); + EXPECT_CALL(consumer_, OnGetTokenSuccess("at1", _)).Times(1); + fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_MakeGetAccessTokenBody) { + { // No scope. + std::string body = + "client_id=cid1&" + "client_secret=cs1&" + "grant_type=refresh_token&" + "refresh_token=rt1"; + EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", ScopeList())); + } + + { // One scope. + std::string body = + "client_id=cid1&" + "client_secret=cs1&" + "grant_type=refresh_token&" + "refresh_token=rt1&" + "scope=https://www.googleapis.com/foo"; + ScopeList scopes; + scopes.push_back("https://www.googleapis.com/foo"); + EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", scopes)); + } + + { // Multiple scopes. + std::string body = + "client_id=cid1&" + "client_secret=cs1&" + "grant_type=refresh_token&" + "refresh_token=rt1&" + "scope=https://www.googleapis.com/foo+" + "https://www.googleapis.com/bar+" + "https://www.googleapis.com/baz"; + ScopeList scopes; + scopes.push_back("https://www.googleapis.com/foo"); + scopes.push_back("https://www.googleapis.com/bar"); + scopes.push_back("https://www.googleapis.com/baz"); + EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", scopes)); + } +} + +// http://crbug.com/114215 +#if defined(OS_WIN) +#define MAYBE_ParseGetAccessTokenResponse DISABLED_ParseGetAccessTokenResponse +#else +#define MAYBE_ParseGetAccessTokenResponse ParseGetAccessTokenResponse +#endif // defined(OS_WIN) +TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { + { // No body. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + + std::string at; + int expires_in; + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at, &expires_in)); + EXPECT_TRUE(at.empty()); + } + { // Bad json. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString("foo"); + + std::string at; + int expires_in; + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at, &expires_in)); + EXPECT_TRUE(at.empty()); + } + { // Valid json: access token missing. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kTokenResponseNoAccessToken); + + std::string at; + int expires_in; + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at, &expires_in)); + EXPECT_TRUE(at.empty()); + } + { // Valid json: all good. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kValidTokenResponse); + + std::string at; + int expires_in; + EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at, &expires_in)); + EXPECT_EQ("at1", at); + EXPECT_EQ(3600, expires_in); + } +} diff --git a/google_apis/gaia/oauth2_api_call_flow.cc b/google_apis/gaia/oauth2_api_call_flow.cc new file mode 100644 index 0000000..9037764 --- /dev/null +++ b/google_apis/gaia/oauth2_api_call_flow.cc @@ -0,0 +1,167 @@ +// 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 "google_apis/gaia/oauth2_api_call_flow.h" + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/stringprintf.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using net::ResponseCookies; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { +static const char kAuthorizationHeaderFormat[] = + "Authorization: Bearer %s"; + +static std::string MakeAuthorizationHeader(const std::string& auth_token) { + return StringPrintf(kAuthorizationHeaderFormat, auth_token.c_str()); +} +} // namespace + +OAuth2ApiCallFlow::OAuth2ApiCallFlow( + net::URLRequestContextGetter* context, + const std::string& refresh_token, + const std::string& access_token, + const std::vector<std::string>& scopes) + : context_(context), + refresh_token_(refresh_token), + access_token_(access_token), + scopes_(scopes), + state_(INITIAL), + tried_mint_access_token_(false) { +} + +OAuth2ApiCallFlow::~OAuth2ApiCallFlow() {} + +void OAuth2ApiCallFlow::Start() { + BeginApiCall(); +} + +void OAuth2ApiCallFlow::BeginApiCall() { + CHECK(state_ == INITIAL || state_ == MINT_ACCESS_TOKEN_DONE); + + // If the access token is empty then directly try to mint one. + if (access_token_.empty()) { + BeginMintAccessToken(); + } else { + state_ = API_CALL_STARTED; + url_fetcher_.reset(CreateURLFetcher()); + url_fetcher_->Start(); // OnURLFetchComplete will be called. + } +} + +void OAuth2ApiCallFlow::EndApiCall(const net::URLFetcher* source) { + CHECK_EQ(API_CALL_STARTED, state_); + state_ = API_CALL_DONE; + + URLRequestStatus status = source->GetStatus(); + if (!status.is_success()) { + state_ = ERROR_STATE; + ProcessApiCallFailure(source); + return; + } + + // If the response code is 401 Unauthorized then access token may have + // expired. So try generating a new access token. + if (source->GetResponseCode() == net::HTTP_UNAUTHORIZED) { + // If we already tried minting a new access token, don't do it again. + if (tried_mint_access_token_) { + state_ = ERROR_STATE; + ProcessApiCallFailure(source); + } else { + BeginMintAccessToken(); + } + + return; + } + + if (source->GetResponseCode() != net::HTTP_OK) { + state_ = ERROR_STATE; + ProcessApiCallFailure(source); + return; + } + + ProcessApiCallSuccess(source); +} + +void OAuth2ApiCallFlow::BeginMintAccessToken() { + CHECK(state_ == INITIAL || state_ == API_CALL_DONE); + CHECK(!tried_mint_access_token_); + state_ = MINT_ACCESS_TOKEN_STARTED; + tried_mint_access_token_ = true; + + oauth2_access_token_fetcher_.reset(CreateAccessTokenFetcher()); + oauth2_access_token_fetcher_->Start( + GaiaUrls::GetInstance()->oauth2_chrome_client_id(), + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), + refresh_token_, + scopes_); +} + +void OAuth2ApiCallFlow::EndMintAccessToken( + const GoogleServiceAuthError* error) { + CHECK_EQ(MINT_ACCESS_TOKEN_STARTED, state_); + + if (!error) { + state_ = MINT_ACCESS_TOKEN_DONE; + BeginApiCall(); + } else { + state_ = ERROR_STATE; + ProcessMintAccessTokenFailure(*error); + } +} + +OAuth2AccessTokenFetcher* OAuth2ApiCallFlow::CreateAccessTokenFetcher() { + return new OAuth2AccessTokenFetcher(this, context_); +} + +void OAuth2ApiCallFlow::OnURLFetchComplete(const net::URLFetcher* source) { + CHECK(source); + CHECK_EQ(API_CALL_STARTED, state_); + EndApiCall(source); +} + +void OAuth2ApiCallFlow::OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time) { + access_token_ = access_token; + EndMintAccessToken(NULL); +} + +void OAuth2ApiCallFlow::OnGetTokenFailure( + const GoogleServiceAuthError& error) { + EndMintAccessToken(&error); +} + +URLFetcher* OAuth2ApiCallFlow::CreateURLFetcher() { + std::string body = CreateApiCallBody(); + bool empty_body = body.empty(); + URLFetcher* result = net::URLFetcher::Create( + 0, + CreateApiCallUrl(), + empty_body ? URLFetcher::GET : URLFetcher::POST, + this); + + result->SetRequestContext(context_); + result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + result->AddExtraRequestHeader(MakeAuthorizationHeader(access_token_)); + + if (!empty_body) + result->SetUploadData("application/x-www-form-urlencoded", body); + + return result; +} diff --git a/google_apis/gaia/oauth2_api_call_flow.h b/google_apis/gaia/oauth2_api_call_flow.h new file mode 100644 index 0000000..419c842 --- /dev/null +++ b/google_apis/gaia/oauth2_api_call_flow.h @@ -0,0 +1,128 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH2_API_CALL_FLOW_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_API_CALL_FLOW_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/oauth2_access_token_consumer.h" +#include "google_apis/gaia/oauth2_access_token_fetcher.h" +#include "google_apis/gaia/oauth2_mint_token_consumer.h" +#include "google_apis/gaia/oauth2_mint_token_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" + +class GoogleServiceAuthError; +class OAuth2MintTokenFlowTest; + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +} + +// Base class for all classes that implement a flow to call OAuth2 +// enabled APIs. +// +// Given a refresh token, an access token, and a list of scopes an OAuth2 +// enabled API is called in the following way: +// 1. Try the given access token to call the API. +// 2. If that does not work, use the refresh token and scopes to generate +// a new access token. +// 3. Try the new access token to call the API. +// +// This class abstracts the basic steps and exposes template methods +// for sub-classes to implement for API specific details. +class OAuth2ApiCallFlow + : public net::URLFetcherDelegate, + public OAuth2AccessTokenConsumer { + public: + // Creates an instance that works with the given data. + // Note that |access_token| can be empty. In that case, the flow will skip + // the first step (of trying an existing access token). + OAuth2ApiCallFlow( + net::URLRequestContextGetter* context, + const std::string& refresh_token, + const std::string& access_token, + const std::vector<std::string>& scopes); + + virtual ~OAuth2ApiCallFlow(); + + // Start the flow. + virtual void Start(); + + // OAuth2AccessTokenFetcher implementation. + virtual void OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time) OVERRIDE; + virtual void OnGetTokenFailure(const GoogleServiceAuthError& error) OVERRIDE; + + // net::URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + protected: + // Template methods for sub-classes. + + // Methods to help create HTTP request. + virtual GURL CreateApiCallUrl() = 0; + virtual std::string CreateApiCallBody() = 0; + + // Sub-classes can expose an appropriate observer interface by implementing + // these template methods. + // Called when the API call finished successfully. + virtual void ProcessApiCallSuccess(const net::URLFetcher* source) = 0; + // Called when the API call failed. + virtual void ProcessApiCallFailure(const net::URLFetcher* source) = 0; + // Called when a new access token is generated. + virtual void ProcessNewAccessToken(const std::string& access_token) = 0; + virtual void ProcessMintAccessTokenFailure( + const GoogleServiceAuthError& error) = 0; + + private: + enum State { + INITIAL, + API_CALL_STARTED, + API_CALL_DONE, + MINT_ACCESS_TOKEN_STARTED, + MINT_ACCESS_TOKEN_DONE, + ERROR_STATE + }; + + friend class OAuth2ApiCallFlowTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2ApiCallFlowTest, CreateURLFetcher); + + // Helper to create an instance of access token fetcher. + // Caller owns the returned instance. + // Note that this is virtual since it is mocked during unit testing. + virtual OAuth2AccessTokenFetcher* CreateAccessTokenFetcher(); + + // Creates an instance of URLFetcher that does not send or save cookies. + // Template method CreateApiCallUrl is used to get the URL. + // Template method CreateApiCallBody is used to get the body. + // The URLFether's method will be GET if body is empty, POST otherwise. + // Caller owns the returned instance. + // Note that this is virtual since it is mocked during unit testing. + virtual net::URLFetcher* CreateURLFetcher(); + + // Helper methods to implement the state machine for the flow. + void BeginApiCall(); + void EndApiCall(const net::URLFetcher* source); + void BeginMintAccessToken(); + void EndMintAccessToken(const GoogleServiceAuthError* error); + + net::URLRequestContextGetter* context_; + std::string refresh_token_; + std::string access_token_; + std::vector<std::string> scopes_; + + State state_; + // Whether we have already tried minting an access token once. + bool tried_mint_access_token_; + + scoped_ptr<net::URLFetcher> url_fetcher_; + scoped_ptr<OAuth2AccessTokenFetcher> oauth2_access_token_fetcher_; + + DISALLOW_COPY_AND_ASSIGN(OAuth2ApiCallFlow); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_API_CALL_FLOW_H_ diff --git a/google_apis/gaia/oauth2_api_call_flow_unittest.cc b/google_apis/gaia/oauth2_api_call_flow_unittest.cc new file mode 100644 index 0000000..137eaeb --- /dev/null +++ b/google_apis/gaia/oauth2_api_call_flow_unittest.cc @@ -0,0 +1,301 @@ +// 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. +// +// A complete set of unit tests for OAuth2MintTokenFlow. + +#include <string> +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "base/time.h" +#include "chrome/test/base/testing_profile.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 "google_apis/gaia/oauth2_api_call_flow.h" +#include "net/http/http_request_headers.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_fetcher_factory.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_status.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using net::HttpRequestHeaders; +using net::ScopedURLFetcherFactory; +using net::TestURLFetcher; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLFetcherFactory; +using net::URLRequestStatus; +using testing::_; +using testing::Return; + +namespace { + +static std::string CreateBody() { + return "some body"; +} + +static GURL CreateApiUrl() { + return GURL("https://www.googleapis.com/someapi"); +} + +static std::vector<std::string> CreateTestScopes() { + std::vector<std::string> scopes; + scopes.push_back("scope1"); + scopes.push_back("scope2"); + return scopes; +} + +class MockUrlFetcherFactory : public ScopedURLFetcherFactory, + public URLFetcherFactory { + public: + MockUrlFetcherFactory() + : ScopedURLFetcherFactory(ALLOW_THIS_IN_INITIALIZER_LIST(this)) { + } + virtual ~MockUrlFetcherFactory() {} + + MOCK_METHOD4( + CreateURLFetcher, + URLFetcher* (int id, + const GURL& url, + URLFetcher::RequestType request_type, + URLFetcherDelegate* d)); +}; + +class MockAccessTokenFetcher : public OAuth2AccessTokenFetcher { + public: + MockAccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, + net::URLRequestContextGetter* getter) + : OAuth2AccessTokenFetcher(consumer, getter) {} + ~MockAccessTokenFetcher() {} + + MOCK_METHOD4(Start, + void (const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes)); +}; + +class MockApiCallFlow : public OAuth2ApiCallFlow { + public: + MockApiCallFlow(net::URLRequestContextGetter* context, + const std::string& refresh_token, + const std::string& access_token, + const std::vector<std::string>& scopes) + : OAuth2ApiCallFlow(context, refresh_token, access_token, scopes) {} + ~MockApiCallFlow() {} + + MOCK_METHOD0(CreateApiCallUrl, GURL ()); + MOCK_METHOD0(CreateApiCallBody, std::string ()); + MOCK_METHOD1(ProcessApiCallSuccess, + void (const URLFetcher* source)); + MOCK_METHOD1(ProcessApiCallFailure, + void (const URLFetcher* source)); + MOCK_METHOD1(ProcessNewAccessToken, + void (const std::string& access_token)); + MOCK_METHOD1(ProcessMintAccessTokenFailure, + void (const GoogleServiceAuthError& error)); + MOCK_METHOD0(CreateAccessTokenFetcher, OAuth2AccessTokenFetcher* ()); +}; + +} // namespace + +class OAuth2ApiCallFlowTest : public testing::Test { + public: + OAuth2ApiCallFlowTest() {} + virtual ~OAuth2ApiCallFlowTest() {} + + protected: + void SetupAccessTokenFetcher( + const std::string& rt, const std::vector<std::string>& scopes) { + EXPECT_CALL(*access_token_fetcher_, + Start(GaiaUrls::GetInstance()->oauth2_chrome_client_id(), + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), + rt, scopes)) + .Times(1); + EXPECT_CALL(*flow_, CreateAccessTokenFetcher()) + .WillOnce(Return(access_token_fetcher_.release())); + } + + TestURLFetcher* CreateURLFetcher( + const GURL& url, bool fetch_succeeds, + int response_code, const std::string& body) { + TestURLFetcher* url_fetcher = new TestURLFetcher(0, url, flow_.get()); + URLRequestStatus::Status status = + fetch_succeeds ? URLRequestStatus::SUCCESS : URLRequestStatus::FAILED; + url_fetcher->set_status(URLRequestStatus(status, 0)); + + if (response_code != 0) + url_fetcher->set_response_code(response_code); + + if (!body.empty()) + url_fetcher->SetResponseString(body); + + return url_fetcher; + } + + void CreateFlow(const std::string& refresh_token, + const std::string& access_token, + const std::vector<std::string>& scopes) { + flow_.reset(new MockApiCallFlow( + profile_.GetRequestContext(), + refresh_token, + access_token, + scopes)); + access_token_fetcher_.reset(new MockAccessTokenFetcher( + flow_.get(), profile_.GetRequestContext())); + } + + TestURLFetcher* SetupApiCall(bool succeeds, net::HttpStatusCode status) { + std::string body(CreateBody()); + GURL url(CreateApiUrl()); + EXPECT_CALL(*flow_, CreateApiCallBody()).WillOnce(Return(body)); + EXPECT_CALL(*flow_, CreateApiCallUrl()).WillOnce(Return(url)); + TestURLFetcher* url_fetcher = CreateURLFetcher( + url, succeeds, status, ""); + EXPECT_CALL(factory_, CreateURLFetcher(_, url, _, _)) + .WillOnce(Return(url_fetcher)); + return url_fetcher; + } + + MockUrlFetcherFactory factory_; + scoped_ptr<MockApiCallFlow> flow_; + scoped_ptr<MockAccessTokenFetcher> access_token_fetcher_; + TestingProfile profile_; +}; + +TEST_F(OAuth2ApiCallFlowTest, FirstApiCallSucceeds) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, at, scopes); + TestURLFetcher* url_fetcher = SetupApiCall(true, net::HTTP_OK); + EXPECT_CALL(*flow_, ProcessApiCallSuccess(url_fetcher)); + flow_->Start(); + flow_->OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2ApiCallFlowTest, SecondApiCallSucceeds) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, at, scopes); + TestURLFetcher* url_fetcher1 = SetupApiCall(true, net::HTTP_UNAUTHORIZED); + flow_->Start(); + SetupAccessTokenFetcher(rt, scopes); + flow_->OnURLFetchComplete(url_fetcher1); + TestURLFetcher* url_fetcher2 = SetupApiCall(true, net::HTTP_OK); + EXPECT_CALL(*flow_, ProcessApiCallSuccess(url_fetcher2)); + flow_->OnGetTokenSuccess( + at, + base::Time::Now() + base::TimeDelta::FromMinutes(3600)); + flow_->OnURLFetchComplete(url_fetcher2); +} + +TEST_F(OAuth2ApiCallFlowTest, SecondApiCallFails) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, at, scopes); + TestURLFetcher* url_fetcher1 = SetupApiCall(true, net::HTTP_UNAUTHORIZED); + flow_->Start(); + SetupAccessTokenFetcher(rt, scopes); + flow_->OnURLFetchComplete(url_fetcher1); + TestURLFetcher* url_fetcher2 = SetupApiCall(false, net::HTTP_UNAUTHORIZED); + EXPECT_CALL(*flow_, ProcessApiCallFailure(url_fetcher2)); + flow_->OnGetTokenSuccess( + at, + base::Time::Now() + base::TimeDelta::FromMinutes(3600)); + flow_->OnURLFetchComplete(url_fetcher2); +} + +TEST_F(OAuth2ApiCallFlowTest, NewTokenGenerationFails) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, at, scopes); + TestURLFetcher* url_fetcher = SetupApiCall(true, net::HTTP_UNAUTHORIZED); + flow_->Start(); + SetupAccessTokenFetcher(rt, scopes); + flow_->OnURLFetchComplete(url_fetcher); + GoogleServiceAuthError error( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + EXPECT_CALL(*flow_, ProcessMintAccessTokenFailure(error)); + flow_->OnGetTokenFailure(error); +} + +TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenFirstApiCallSucceeds) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, "", scopes); + SetupAccessTokenFetcher(rt, scopes); + TestURLFetcher* url_fetcher = SetupApiCall(true, net::HTTP_OK); + EXPECT_CALL(*flow_, ProcessApiCallSuccess(url_fetcher)); + flow_->Start(); + flow_->OnGetTokenSuccess( + at, + base::Time::Now() + base::TimeDelta::FromMinutes(3600)); + flow_->OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenApiCallFails) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, "", scopes); + SetupAccessTokenFetcher(rt, scopes); + TestURLFetcher* url_fetcher = SetupApiCall(false, net::HTTP_BAD_GATEWAY); + EXPECT_CALL(*flow_, ProcessApiCallFailure(url_fetcher)); + flow_->Start(); + flow_->OnGetTokenSuccess( + at, + base::Time::Now() + base::TimeDelta::FromMinutes(3600)); + flow_->OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenNewTokenGenerationFails) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, "", scopes); + SetupAccessTokenFetcher(rt, scopes); + GoogleServiceAuthError error( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + EXPECT_CALL(*flow_, ProcessMintAccessTokenFailure(error)); + flow_->Start(); + flow_->OnGetTokenFailure(error); +} + +TEST_F(OAuth2ApiCallFlowTest, CreateURLFetcher) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + std::string body = CreateBody(); + GURL url(CreateApiUrl()); + + CreateFlow(rt, at, scopes); + scoped_ptr<TestURLFetcher> url_fetcher(SetupApiCall(true, net::HTTP_OK)); + flow_->CreateURLFetcher(); + HttpRequestHeaders headers; + url_fetcher->GetExtraRequestHeaders(&headers); + std::string auth_header; + EXPECT_TRUE(headers.GetHeader("Authorization", &auth_header)); + EXPECT_EQ("Bearer access_token", auth_header); + EXPECT_EQ(url, url_fetcher->GetOriginalURL()); + EXPECT_EQ(body, url_fetcher->upload_data()); +} diff --git a/google_apis/gaia/oauth2_mint_token_consumer.h b/google_apis/gaia/oauth2_mint_token_consumer.h new file mode 100644 index 0000000..95ea12e --- /dev/null +++ b/google_apis/gaia/oauth2_mint_token_consumer.h @@ -0,0 +1,22 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_CONSUMER_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_CONSUMER_H_ + +#include <string> + +class GoogleServiceAuthError; + +// An interface that defines the callbacks for consumers to which +// OAuth2MintTokenFetcher can return results. +class OAuth2MintTokenConsumer { + public: + virtual ~OAuth2MintTokenConsumer() {} + + virtual void OnMintTokenSuccess(const std::string& access_token) {} + virtual void OnMintTokenFailure(const GoogleServiceAuthError& error) {} +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_CONSUMER_H_ diff --git a/google_apis/gaia/oauth2_mint_token_fetcher.cc b/google_apis/gaia/oauth2_mint_token_fetcher.cc new file mode 100644 index 0000000..425a126 --- /dev/null +++ b/google_apis/gaia/oauth2_mint_token_fetcher.cc @@ -0,0 +1,190 @@ +// 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 "google_apis/gaia/oauth2_mint_token_fetcher.h" + +#include <algorithm> +#include <string> + +#include "base/json/json_reader.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::ResponseCookies; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { +static const char kAuthorizationHeaderFormat[] = + "Authorization: Bearer %s"; +static const char kOAuth2IssueTokenBodyFormat[] = + "force=true" + "&response_type=token" + "&scope=%s" + "&client_id=%s" + "&origin=%s"; +static const char kAccessTokenKey[] = "token"; + +static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) { + CHECK(!status.is_success()); + if (status.status() == URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } +} + +static URLFetcher* CreateFetcher(URLRequestContextGetter* getter, + const GURL& url, + const std::string& headers, + const std::string& body, + URLFetcherDelegate* delegate) { + bool empty_body = body.empty(); + URLFetcher* result = net::URLFetcher::Create( + 0, url, + empty_body ? URLFetcher::GET : URLFetcher::POST, + delegate); + + result->SetRequestContext(getter); + result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + + if (!empty_body) + result->SetUploadData("application/x-www-form-urlencoded", body); + if (!headers.empty()) + result->SetExtraRequestHeaders(headers); + + return result; +} +} // namespace + +OAuth2MintTokenFetcher::OAuth2MintTokenFetcher( + OAuth2MintTokenConsumer* consumer, + URLRequestContextGetter* getter, + const std::string& source) + : consumer_(consumer), + getter_(getter), + source_(source), + state_(INITIAL) { } + +OAuth2MintTokenFetcher::~OAuth2MintTokenFetcher() { } + +void OAuth2MintTokenFetcher::CancelRequest() { + fetcher_.reset(); +} + +void OAuth2MintTokenFetcher::Start(const std::string& oauth_login_access_token, + const std::string& client_id, + const std::vector<std::string>& scopes, + const std::string& origin) { + oauth_login_access_token_ = oauth_login_access_token; + client_id_ = client_id; + scopes_ = scopes; + origin_ = origin; + StartMintToken(); +} + +void OAuth2MintTokenFetcher::StartMintToken() { + CHECK_EQ(INITIAL, state_); + state_ = MINT_TOKEN_STARTED; + fetcher_.reset(CreateFetcher( + getter_, + MakeMintTokenUrl(), + MakeMintTokenHeader(oauth_login_access_token_), + MakeMintTokenBody(client_id_, scopes_, origin_), + this)); + fetcher_->Start(); // OnURLFetchComplete will be called. +} + +void OAuth2MintTokenFetcher::EndMintToken(const net::URLFetcher* source) { + CHECK_EQ(MINT_TOKEN_STARTED, state_); + state_ = MINT_TOKEN_DONE; + + URLRequestStatus status = source->GetStatus(); + if (!status.is_success()) { + OnMintTokenFailure(CreateAuthError(status)); + return; + } + + if (source->GetResponseCode() != net::HTTP_OK) { + OnMintTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + return; + } + + // The request was successfully fetched and it returned OK. + // Parse out the access token. + std::string access_token; + ParseMintTokenResponse(source, &access_token); + OnMintTokenSuccess(access_token); +} + +void OAuth2MintTokenFetcher::OnMintTokenSuccess( + const std::string& access_token) { + consumer_->OnMintTokenSuccess(access_token); +} + +void OAuth2MintTokenFetcher::OnMintTokenFailure( + const GoogleServiceAuthError& error) { + state_ = ERROR_STATE; + consumer_->OnMintTokenFailure(error); +} + +void OAuth2MintTokenFetcher::OnURLFetchComplete(const net::URLFetcher* source) { + CHECK(source); + CHECK_EQ(MINT_TOKEN_STARTED, state_); + EndMintToken(source); +} + +// static +GURL OAuth2MintTokenFetcher::MakeMintTokenUrl() { + return GURL(GaiaUrls::GetInstance()->oauth2_issue_token_url()); +} + +// static +std::string OAuth2MintTokenFetcher::MakeMintTokenHeader( + const std::string& access_token) { + return StringPrintf(kAuthorizationHeaderFormat, access_token.c_str()); +} + +// static +std::string OAuth2MintTokenFetcher::MakeMintTokenBody( + const std::string& client_id, + const std::vector<std::string>& scopes, + const std::string& origin) { + return StringPrintf( + kOAuth2IssueTokenBodyFormat, + net::EscapeUrlEncodedData(JoinString(scopes, ','), true).c_str(), + net::EscapeUrlEncodedData(client_id, true).c_str(), + net::EscapeUrlEncodedData(origin, true).c_str()); +} + +// static +bool OAuth2MintTokenFetcher::ParseMintTokenResponse( + const net::URLFetcher* source, + std::string* access_token) { + CHECK(source); + CHECK(access_token); + std::string data; + source->GetResponseAsString(&data); + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + return false; + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + return dict->GetString(kAccessTokenKey, access_token); +} diff --git a/google_apis/gaia/oauth2_mint_token_fetcher.h b/google_apis/gaia/oauth2_mint_token_fetcher.h new file mode 100644 index 0000000..4995c7a --- /dev/null +++ b/google_apis/gaia/oauth2_mint_token_fetcher.h @@ -0,0 +1,104 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FETCHER_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FETCHER_H_ + +#include <string> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/oauth2_mint_token_consumer.h" +#include "googleurl/src/gurl.h" +#include "net/url_request/url_fetcher_delegate.h" + +class OAuth2MintTokenFetcherTest; + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +class URLRequestStatus; +} + +// Abstracts the details to mint new OAuth2 tokens from OAuth2 login scoped +// token. +// +// This class should be used on a single thread, but it can be whichever thread +// that you like. +// Also, do not reuse the same instance. Once Start() is called, the instance +// should not be reused. +// +// Usage: +// * Create an instance with a consumer. +// * Call Start() +// * The consumer passed in the constructor will be called on the same +// thread Start was called with the results. +// +// This class can handle one request at a time. To parallelize requests, +// create multiple instances. +class OAuth2MintTokenFetcher : public net::URLFetcherDelegate { + public: + OAuth2MintTokenFetcher(OAuth2MintTokenConsumer* consumer, + net::URLRequestContextGetter* getter, + const std::string& source); + virtual ~OAuth2MintTokenFetcher(); + + // Start the flow. + virtual void Start(const std::string& oauth_login_access_token, + const std::string& client_id, + const std::vector<std::string>& scopes, + const std::string& origin); + + void CancelRequest(); + + // Implementation of net::URLFetcherDelegate + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + enum State { + INITIAL, + MINT_TOKEN_STARTED, + MINT_TOKEN_DONE, + ERROR_STATE, + }; + + // Helper methods for the flow. + void StartMintToken(); + void EndMintToken(const net::URLFetcher* source); + + // Helper methods for reporting back results. + void OnMintTokenSuccess(const std::string& access_token); + void OnMintTokenFailure(const GoogleServiceAuthError& error); + + // Other helpers. + static GURL MakeMintTokenUrl(); + static std::string MakeMintTokenHeader(const std::string& access_token); + static std::string MakeMintTokenBody(const std::string& client_id, + const std::vector<std::string>& scopes, + const std::string& origin); + static bool ParseMintTokenResponse(const net::URLFetcher* source, + std::string* access_token); + + // State that is set during construction. + OAuth2MintTokenConsumer* const consumer_; + net::URLRequestContextGetter* const getter_; + std::string source_; + State state_; + + // While a fetch is in progress. + scoped_ptr<net::URLFetcher> fetcher_; + std::string oauth_login_access_token_; + std::string client_id_; + std::vector<std::string> scopes_; + std::string origin_; + + friend class OAuth2MintTokenFetcherTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFetcherTest, + ParseMintTokenResponse); + + DISALLOW_COPY_AND_ASSIGN(OAuth2MintTokenFetcher); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FETCHER_H_ diff --git a/google_apis/gaia/oauth2_mint_token_fetcher_unittest.cc b/google_apis/gaia/oauth2_mint_token_fetcher_unittest.cc new file mode 100644 index 0000000..fda775b --- /dev/null +++ b/google_apis/gaia/oauth2_mint_token_fetcher_unittest.cc @@ -0,0 +1,179 @@ +// 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. +// +// A complete set of unit tests for OAuth2MintTokenFetcher. + +#include <string> +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/test_browser_thread.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_mint_token_consumer.h" +#include "google_apis/gaia/oauth2_mint_token_fetcher.h" +#include "googleurl/src/gurl.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_fetcher_factory.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_status.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using content::BrowserThread; +using net::ResponseCookies; +using net::ScopedURLFetcherFactory; +using net::TestURLFetcher; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLFetcherFactory; +using net::URLRequestStatus; +using testing::_; +using testing::Return; + +namespace { + +static const char kValidTokenResponse[] = + "{" + " \"token\": \"at1\"," + " \"issueAdvice\": \"Auto\"" + "}"; +static const char kTokenResponseNoAccessToken[] = + "{" + " \"issueAdvice\": \"Auto\"" + "}"; + +class MockUrlFetcherFactory : public ScopedURLFetcherFactory, + public URLFetcherFactory { +public: + MockUrlFetcherFactory() + : ScopedURLFetcherFactory(ALLOW_THIS_IN_INITIALIZER_LIST(this)) { + } + virtual ~MockUrlFetcherFactory() {} + + MOCK_METHOD4( + CreateURLFetcher, + URLFetcher* (int id, + const GURL& url, + URLFetcher::RequestType request_type, + URLFetcherDelegate* d)); +}; + +class MockOAuth2MintTokenConsumer : public OAuth2MintTokenConsumer { + public: + MockOAuth2MintTokenConsumer() {} + ~MockOAuth2MintTokenConsumer() {} + + MOCK_METHOD1(OnMintTokenSuccess, void(const std::string& access_token)); + MOCK_METHOD1(OnMintTokenFailure, + void(const GoogleServiceAuthError& error)); +}; + +} // namespace + +class OAuth2MintTokenFetcherTest : public testing::Test { + public: + OAuth2MintTokenFetcherTest() + : ui_thread_(BrowserThread::UI, &message_loop_), + fetcher_(&consumer_, profile_.GetRequestContext(), "test") { + test_scopes_.push_back("scope1"); + test_scopes_.push_back("scope1"); + } + + virtual ~OAuth2MintTokenFetcherTest() { } + + virtual TestURLFetcher* SetupIssueToken( + bool fetch_succeeds, int response_code, const std::string& body) { + GURL url(GaiaUrls::GetInstance()->oauth2_issue_token_url()); + TestURLFetcher* url_fetcher = new TestURLFetcher(0, url, &fetcher_); + URLRequestStatus::Status status = + fetch_succeeds ? URLRequestStatus::SUCCESS : URLRequestStatus::FAILED; + url_fetcher->set_status(URLRequestStatus(status, 0)); + + if (response_code != 0) + url_fetcher->set_response_code(response_code); + + if (!body.empty()) + url_fetcher->SetResponseString(body); + + EXPECT_CALL(factory_, CreateURLFetcher(_, url, _, _)) + .WillOnce(Return(url_fetcher)); + return url_fetcher; + } + + protected: + MessageLoop message_loop_; + content::TestBrowserThread ui_thread_; + MockUrlFetcherFactory factory_; + MockOAuth2MintTokenConsumer consumer_; + TestingProfile profile_; + OAuth2MintTokenFetcher fetcher_; + std::vector<std::string> test_scopes_; +}; + +TEST_F(OAuth2MintTokenFetcherTest, GetAccessTokenRequestFailure) { + TestURLFetcher* url_fetcher = SetupIssueToken(false, 0, ""); + EXPECT_CALL(consumer_, OnMintTokenFailure(_)).Times(1); + fetcher_.Start("access_token1", "client1", test_scopes_, "extension1"); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2MintTokenFetcherTest, GetAccessTokenResponseCodeFailure) { + TestURLFetcher* url_fetcher = SetupIssueToken( + false, net::HTTP_FORBIDDEN, ""); + EXPECT_CALL(consumer_, OnMintTokenFailure(_)).Times(1); + fetcher_.Start("access_token1", "client1", test_scopes_, "extension1"); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2MintTokenFetcherTest, Success) { + TestURLFetcher* url_fetcher = SetupIssueToken( + true, net::HTTP_OK, kValidTokenResponse); + EXPECT_CALL(consumer_, OnMintTokenSuccess("at1")).Times(1); + fetcher_.Start("access_token1", "client1", test_scopes_, "extension1"); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2MintTokenFetcherTest, ParseMintTokenResponse) { + { // No body. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + + std::string at; + EXPECT_FALSE(OAuth2MintTokenFetcher::ParseMintTokenResponse( + &url_fetcher, &at)); + EXPECT_TRUE(at.empty()); + } + { // Bad json. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString("foo"); + + std::string at; + EXPECT_FALSE(OAuth2MintTokenFetcher::ParseMintTokenResponse( + &url_fetcher, &at)); + EXPECT_TRUE(at.empty()); + } + { // Valid json: access token missing. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kTokenResponseNoAccessToken); + + std::string at; + EXPECT_FALSE(OAuth2MintTokenFetcher::ParseMintTokenResponse( + &url_fetcher, &at)); + EXPECT_TRUE(at.empty()); + } + { // Valid json: all good. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kValidTokenResponse); + + std::string at; + EXPECT_TRUE(OAuth2MintTokenFetcher::ParseMintTokenResponse( + &url_fetcher, &at)); + EXPECT_EQ("at1", at); + } +} diff --git a/google_apis/gaia/oauth2_mint_token_flow.cc b/google_apis/gaia/oauth2_mint_token_flow.cc new file mode 100644 index 0000000..66dd1a0 --- /dev/null +++ b/google_apis/gaia/oauth2_mint_token_flow.cc @@ -0,0 +1,261 @@ +// 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 "google_apis/gaia/oauth2_mint_token_flow.h" + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/command_line.h" +#include "base/json/json_reader.h" +#include "base/message_loop.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/escape.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using net::URLFetcher; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { + +static const char kForceValueFalse[] = "false"; +static const char kForceValueTrue[] = "true"; +static const char kResponseTypeValueNone[] = "none"; +static const char kResponseTypeValueToken[] = "token"; + +static const char kOAuth2IssueTokenBodyFormat[] = + "force=%s" + "&response_type=%s" + "&scope=%s" + "&client_id=%s" + "&origin=%s"; +static const char kIssueAdviceKey[] = "issueAdvice"; +static const char kIssueAdviceValueAuto[] = "auto"; +static const char kIssueAdviceValueConsent[] = "consent"; +static const char kAccessTokenKey[] = "token"; +static const char kConsentKey[] = "consent"; +static const char kScopesKey[] = "scopes"; +static const char kDescriptionKey[] = "description"; +static const char kDetailKey[] = "detail"; +static const char kDetailSeparators[] = "\n"; + +static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) { + if (status.status() == URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + // TODO(munjal): Improve error handling. Currently we return connection + // error for even application level errors. We need to either expand the + // GoogleServiceAuthError enum or create a new one to report better + // errors. + DLOG(WARNING) << "Server returned error: errno " << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } +} + +} // namespace + +IssueAdviceInfoEntry::IssueAdviceInfoEntry() {} +IssueAdviceInfoEntry::~IssueAdviceInfoEntry() {} + +bool IssueAdviceInfoEntry::operator ==(const IssueAdviceInfoEntry& rhs) const { + return description == rhs.description && details == rhs.details; +} + +OAuth2MintTokenFlow::Parameters::Parameters() : mode(MODE_ISSUE_ADVICE) {} + +OAuth2MintTokenFlow::Parameters::Parameters( + const std::string& rt, + const std::string& eid, + const std::string& cid, + const std::vector<std::string>& scopes_arg, + Mode mode_arg) + : login_refresh_token(rt), + extension_id(eid), + client_id(cid), + scopes(scopes_arg), + mode(mode_arg) { +} + +OAuth2MintTokenFlow::Parameters::~Parameters() {} + +OAuth2MintTokenFlow::OAuth2MintTokenFlow( + URLRequestContextGetter* context, + Delegate* delegate, + const Parameters& parameters) + : OAuth2ApiCallFlow( + context, parameters.login_refresh_token, + "", std::vector<std::string>()), + context_(context), + delegate_(delegate), + parameters_(parameters), + delete_when_done_(false), + ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)) { +} + +OAuth2MintTokenFlow::~OAuth2MintTokenFlow() { } + +void OAuth2MintTokenFlow::FireAndForget() { + delete_when_done_ = true; + Start(); +} + +void OAuth2MintTokenFlow::ReportSuccess(const std::string& access_token) { + scoped_ptr<OAuth2MintTokenFlow> will_delete(delete_when_done_ ? this : NULL); + + if (delegate_) + delegate_->OnMintTokenSuccess(access_token); + + // |this| may already be deleted. +} + +void OAuth2MintTokenFlow::ReportIssueAdviceSuccess( + const IssueAdviceInfo& issue_advice) { + scoped_ptr<OAuth2MintTokenFlow> will_delete(delete_when_done_ ? this : NULL); + + if (delegate_) + delegate_->OnIssueAdviceSuccess(issue_advice); + + // |this| may already be deleted. +} + +void OAuth2MintTokenFlow::ReportFailure( + const GoogleServiceAuthError& error) { + scoped_ptr<OAuth2MintTokenFlow> will_delete(delete_when_done_ ? this : NULL); + + if (delegate_) + delegate_->OnMintTokenFailure(error); + + // |this| may already be deleted. +} + +GURL OAuth2MintTokenFlow::CreateApiCallUrl() { + return GURL(GaiaUrls::GetInstance()->oauth2_issue_token_url()); +} + +std::string OAuth2MintTokenFlow::CreateApiCallBody() { + const char* force_value = + (parameters_.mode == MODE_MINT_TOKEN_FORCE || + parameters_.mode == MODE_RECORD_GRANT) + ? kForceValueTrue : kForceValueFalse; + const char* response_type_value = + (parameters_.mode == MODE_MINT_TOKEN_NO_FORCE || + parameters_.mode == MODE_MINT_TOKEN_FORCE) + ? kResponseTypeValueToken : kResponseTypeValueNone; + return StringPrintf( + kOAuth2IssueTokenBodyFormat, + net::EscapeUrlEncodedData(force_value, true).c_str(), + net::EscapeUrlEncodedData(response_type_value, true).c_str(), + net::EscapeUrlEncodedData( + JoinString(parameters_.scopes, ' '), true).c_str(), + net::EscapeUrlEncodedData(parameters_.client_id, true).c_str(), + net::EscapeUrlEncodedData(parameters_.extension_id, true).c_str()); +} + +void OAuth2MintTokenFlow::ProcessApiCallSuccess( + const net::URLFetcher* source) { + // TODO(munjal): Change error code paths in this method to report an + // internal error. + std::string response_body; + source->GetResponseAsString(&response_body); + scoped_ptr<base::Value> value(base::JSONReader::Read(response_body)); + DictionaryValue* dict = NULL; + if (!value.get() || !value->GetAsDictionary(&dict)) { + ReportFailure(GoogleServiceAuthError::FromConnectionError(101)); + return; + } + + std::string issue_advice; + if (!dict->GetString(kIssueAdviceKey, &issue_advice)) { + ReportFailure(GoogleServiceAuthError::FromConnectionError(101)); + return; + } + if (issue_advice == kIssueAdviceValueConsent) { + IssueAdviceInfo issue_advice; + if (ParseIssueAdviceResponse(dict, &issue_advice)) + ReportIssueAdviceSuccess(issue_advice); + else + ReportFailure(GoogleServiceAuthError::FromConnectionError(101)); + } else { + std::string access_token; + if (ParseMintTokenResponse(dict, &access_token)) + ReportSuccess(access_token); + else + ReportFailure(GoogleServiceAuthError::FromConnectionError(101)); + } + + // |this| may be deleted! +} + +void OAuth2MintTokenFlow::ProcessApiCallFailure( + const net::URLFetcher* source) { + ReportFailure(CreateAuthError(source->GetStatus())); +} +void OAuth2MintTokenFlow::ProcessNewAccessToken( + const std::string& access_token) { + // We don't currently store new access tokens. We generate one every time. + // So we have nothing to do here. + return; +} +void OAuth2MintTokenFlow::ProcessMintAccessTokenFailure( + const GoogleServiceAuthError& error) { + ReportFailure(error); +} + +// static +bool OAuth2MintTokenFlow::ParseMintTokenResponse( + const base::DictionaryValue* dict, std::string* access_token) { + CHECK(dict); + CHECK(access_token); + return dict->GetString(kAccessTokenKey, access_token); +} + +// static +bool OAuth2MintTokenFlow::ParseIssueAdviceResponse( + const base::DictionaryValue* dict, IssueAdviceInfo* issue_advice) { + CHECK(dict); + CHECK(issue_advice); + + const base::DictionaryValue* consent_dict = NULL; + if (!dict->GetDictionary(kConsentKey, &consent_dict)) + return false; + + const base::ListValue* scopes_list = NULL; + if (!consent_dict->GetList(kScopesKey, &scopes_list)) + return false; + + bool success = true; + for (size_t index = 0; index < scopes_list->GetSize(); ++index) { + const base::DictionaryValue* scopes_entry = NULL; + IssueAdviceInfoEntry entry; + string16 detail; + if (!scopes_list->GetDictionary(index, &scopes_entry) || + !scopes_entry->GetString(kDescriptionKey, &entry.description) || + !scopes_entry->GetString(kDetailKey, &detail)) { + success = false; + break; + } + + TrimWhitespace(entry.description, TRIM_ALL, &entry.description); + static const string16 detail_separators = ASCIIToUTF16(kDetailSeparators); + Tokenize(detail, detail_separators, &entry.details); + for (size_t i = 0; i < entry.details.size(); i++) + TrimWhitespace(entry.details[i], TRIM_ALL, &entry.details[i]); + issue_advice->push_back(entry); + } + + if (!success) + issue_advice->clear(); + + return success; +} diff --git a/google_apis/gaia/oauth2_mint_token_flow.h b/google_apis/gaia/oauth2_mint_token_flow.h new file mode 100644 index 0000000..cdca9ec --- /dev/null +++ b/google_apis/gaia/oauth2_mint_token_flow.h @@ -0,0 +1,153 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FLOW_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FLOW_H_ + +#include <string> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "base/string16.h" +#include "google_apis/gaia/oauth2_api_call_flow.h" + +class GoogleServiceAuthError; +class OAuth2MintTokenFlowTest; + +namespace base { +class DictionaryValue; +} + +namespace content { +class URLFetcher; +} + +namespace net { +class URLRequestContextGetter; +} + +// IssueAdvice: messages to show to the user to get a user's approval. +// The structure is as follows: +// * Description 1 +// - Detail 1.1 +// - Details 1.2 +// * Description 2 +// - Detail 2.1 +// - Detail 2.2 +// - Detail 2.3 +// * Description 3 +// - Detail 3.1 +struct IssueAdviceInfoEntry { + public: + IssueAdviceInfoEntry(); + ~IssueAdviceInfoEntry(); + + string16 description; + std::vector<string16> details; + + bool operator==(const IssueAdviceInfoEntry& rhs) const; +}; + +typedef std::vector<IssueAdviceInfoEntry> IssueAdviceInfo; + +// This class implements the OAuth2 flow to Google to mint an OAuth2 +// token for the given client and the given set of scopes from the +// OAuthLogin scoped "master" OAuth2 token for the user logged in to +// Chrome. +class OAuth2MintTokenFlow : public OAuth2ApiCallFlow { + public: + // There are four differnt modes when minting a token to grant + // access to third-party app for a user. + enum Mode { + // Get the messages to display to the user without minting a token. + MODE_ISSUE_ADVICE, + // Record a grant but do not get a token back. + MODE_RECORD_GRANT, + // Mint a token for an existing grant. + MODE_MINT_TOKEN_NO_FORCE, + // Mint a token forcefully even if there is no existing grant. + MODE_MINT_TOKEN_FORCE, + }; + + // Parameters needed to mint a token. + struct Parameters { + public: + Parameters(); + Parameters(const std::string& rt, + const std::string& eid, + const std::string& cid, + const std::vector<std::string>& scopes_arg, + Mode mode_arg); + ~Parameters(); + + std::string login_refresh_token; + std::string extension_id; + std::string client_id; + std::vector<std::string> scopes; + Mode mode; + }; + + class Delegate { + public: + virtual void OnMintTokenSuccess(const std::string& access_token) {} + virtual void OnIssueAdviceSuccess(const IssueAdviceInfo& issue_advice) {} + virtual void OnMintTokenFailure(const GoogleServiceAuthError& error) {} + + protected: + virtual ~Delegate() {} + }; + + OAuth2MintTokenFlow(net::URLRequestContextGetter* context, + Delegate* delegate, + const Parameters& parameters); + virtual ~OAuth2MintTokenFlow(); + + // Starts the flow, and deletes |this| when done. Useful when the caller + // does not care about the response (|delegate_| is NULL). + void FireAndForget(); + + protected: + // Implementation of template methods in OAuth2ApiCallFlow. + virtual GURL CreateApiCallUrl() OVERRIDE; + virtual std::string CreateApiCallBody() OVERRIDE; + + virtual void ProcessApiCallSuccess( + const net::URLFetcher* source) OVERRIDE; + virtual void ProcessApiCallFailure( + const net::URLFetcher* source) OVERRIDE; + virtual void ProcessNewAccessToken(const std::string& access_token) OVERRIDE; + virtual void ProcessMintAccessTokenFailure( + const GoogleServiceAuthError& error) OVERRIDE; + + private: + friend class OAuth2MintTokenFlowTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, CreateApiCallBody); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, ParseIssueAdviceResponse); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, ParseMintTokenResponse); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, ProcessApiCallSuccess); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, ProcessApiCallFailure); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, + ProcessMintAccessTokenFailure); + + void ReportSuccess(const std::string& access_token); + void ReportIssueAdviceSuccess(const IssueAdviceInfo& issue_advice); + void ReportFailure(const GoogleServiceAuthError& error); + + static bool ParseIssueAdviceResponse( + const base::DictionaryValue* dict, IssueAdviceInfo* issue_advice); + static bool ParseMintTokenResponse( + const base::DictionaryValue* dict, std::string* access_token); + + net::URLRequestContextGetter* context_; + Delegate* delegate_; + Parameters parameters_; + // If true, |this| owns itself and will delete itself after reporting + // success or failure. + bool delete_when_done_; + base::WeakPtrFactory<OAuth2MintTokenFlow> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(OAuth2MintTokenFlow); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FLOW_H_ diff --git a/google_apis/gaia/oauth2_mint_token_flow_unittest.cc b/google_apis/gaia/oauth2_mint_token_flow_unittest.cc new file mode 100644 index 0000000..4b24e939 --- /dev/null +++ b/google_apis/gaia/oauth2_mint_token_flow_unittest.cc @@ -0,0 +1,358 @@ +// 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. +// +// A complete set of unit tests for OAuth2MintTokenFlow. + +#include <string> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/memory/scoped_ptr.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_mint_token_flow.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_status.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using net::TestURLFetcher; +using net::URLFetcher; +using net::URLRequestStatus; +using testing::_; +using testing::StrictMock; + +namespace { + +static const char kValidTokenResponse[] = + "{" + " \"token\": \"at1\"," + " \"issueAdvice\": \"Auto\"" + "}"; +static const char kTokenResponseNoAccessToken[] = + "{" + " \"issueAdvice\": \"Auto\"" + "}"; + +static const char kValidIssueAdviceResponse[] = + "{" + " \"issueAdvice\": \"consent\"," + " \"consent\": {" + " \"oauthClient\": {" + " \"name\": \"Test app\"," + " \"iconUri\": \"\"," + " \"developerEmail\": \"munjal@chromium.org\"" + " }," + " \"scopes\": [" + " {" + " \"description\": \"Manage your calendars\"," + " \"detail\": \"\nView and manage your calendars\n\"" + " }," + " {" + " \"description\": \"Manage your documents\"," + " \"detail\": \"\nView your documents\nUpload new documents\n\"" + " }" + " ]" + " }" + "}"; + +static const char kIssueAdviceResponseNoDescription[] = + "{" + " \"issueAdvice\": \"consent\"," + " \"consent\": {" + " \"oauthClient\": {" + " \"name\": \"Test app\"," + " \"iconUri\": \"\"," + " \"developerEmail\": \"munjal@chromium.org\"" + " }," + " \"scopes\": [" + " {" + " \"description\": \"Manage your calendars\"," + " \"detail\": \"\nView and manage your calendars\n\"" + " }," + " {" + " \"detail\": \"\nView your documents\nUpload new documents\n\"" + " }" + " ]" + " }" + "}"; + +static const char kIssueAdviceResponseNoDetail[] = + "{" + " \"issueAdvice\": \"consent\"," + " \"consent\": {" + " \"oauthClient\": {" + " \"name\": \"Test app\"," + " \"iconUri\": \"\"," + " \"developerEmail\": \"munjal@chromium.org\"" + " }," + " \"scopes\": [" + " {" + " \"description\": \"Manage your calendars\"," + " \"detail\": \"\nView and manage your calendars\n\"" + " }," + " {" + " \"description\": \"Manage your documents\"" + " }" + " ]" + " }" + "}"; + +std::vector<std::string> CreateTestScopes() { + std::vector<std::string> scopes; + scopes.push_back("http://scope1"); + scopes.push_back("http://scope2"); + return scopes; +} + +static IssueAdviceInfo CreateIssueAdvice() { + IssueAdviceInfo ia; + IssueAdviceInfoEntry e1; + e1.description = ASCIIToUTF16("Manage your calendars"); + e1.details.push_back(ASCIIToUTF16("View and manage your calendars")); + ia.push_back(e1); + IssueAdviceInfoEntry e2; + e2.description = ASCIIToUTF16("Manage your documents"); + e2.details.push_back(ASCIIToUTF16("View your documents")); + e2.details.push_back(ASCIIToUTF16("Upload new documents")); + ia.push_back(e2); + return ia; +} + +class MockDelegate : public OAuth2MintTokenFlow::Delegate { + public: + MockDelegate() {} + ~MockDelegate() {} + + MOCK_METHOD1(OnMintTokenSuccess, void(const std::string& access_token)); + MOCK_METHOD1(OnIssueAdviceSuccess, + void (const IssueAdviceInfo& issue_advice)); + MOCK_METHOD1(OnMintTokenFailure, + void(const GoogleServiceAuthError& error)); +}; + +class MockMintTokenFlow : public OAuth2MintTokenFlow { + public: + explicit MockMintTokenFlow(MockDelegate* delegate, + const OAuth2MintTokenFlow::Parameters& parameters ) + : OAuth2MintTokenFlow(NULL, delegate, parameters) {} + ~MockMintTokenFlow() {} + + MOCK_METHOD0(CreateAccessTokenFetcher, OAuth2AccessTokenFetcher*()); + MOCK_METHOD0(CreateMintTokenFetcher, OAuth2MintTokenFetcher*()); +}; + +} // namespace + +class OAuth2MintTokenFlowTest : public testing::Test { + public: + OAuth2MintTokenFlowTest() {} + virtual ~OAuth2MintTokenFlowTest() { } + + protected: + void CreateFlow(OAuth2MintTokenFlow::Mode mode) { + return CreateFlow(&delegate_, mode); + } + + void CreateFlow(MockDelegate* delegate, + OAuth2MintTokenFlow::Mode mode) { + std::string rt = "refresh_token"; + std::string ext_id = "ext1"; + std::string client_id = "client1"; + std::vector<std::string> scopes(CreateTestScopes()); + flow_.reset(new MockMintTokenFlow( + delegate, + OAuth2MintTokenFlow::Parameters(rt, ext_id, client_id, scopes, mode))); + } + + // Helper to parse the given string to DictionaryValue. + static base::DictionaryValue* ParseJson(const std::string& str) { + scoped_ptr<Value> value(base::JSONReader::Read(str)); + EXPECT_TRUE(value.get()); + EXPECT_EQ(Value::TYPE_DICTIONARY, value->GetType()); + return static_cast<base::DictionaryValue*>(value.release()); + } + + scoped_ptr<MockMintTokenFlow> flow_; + StrictMock<MockDelegate> delegate_; +}; + +TEST_F(OAuth2MintTokenFlowTest, CreateApiCallBody) { + { // Issue advice mode. + CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE); + std::string body = flow_->CreateApiCallBody(); + std::string expected_body( + "force=false" + "&response_type=none" + "&scope=http://scope1+http://scope2" + "&client_id=client1" + "&origin=ext1"); + EXPECT_EQ(expected_body, body); + } + { // Record grant mode. + CreateFlow(OAuth2MintTokenFlow::MODE_RECORD_GRANT); + std::string body = flow_->CreateApiCallBody(); + std::string expected_body( + "force=true" + "&response_type=none" + "&scope=http://scope1+http://scope2" + "&client_id=client1" + "&origin=ext1"); + EXPECT_EQ(expected_body, body); + } + { // Mint token no force mode. + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + std::string body = flow_->CreateApiCallBody(); + std::string expected_body( + "force=false" + "&response_type=token" + "&scope=http://scope1+http://scope2" + "&client_id=client1" + "&origin=ext1"); + EXPECT_EQ(expected_body, body); + } + { // Mint token force mode. + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE); + std::string body = flow_->CreateApiCallBody(); + std::string expected_body( + "force=true" + "&response_type=token" + "&scope=http://scope1+http://scope2" + "&client_id=client1" + "&origin=ext1"); + EXPECT_EQ(expected_body, body); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ParseMintTokenResponse) { + { // Access token missing. + scoped_ptr<base::DictionaryValue> json( + ParseJson(kTokenResponseNoAccessToken)); + std::string at; + EXPECT_FALSE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at)); + EXPECT_TRUE(at.empty()); + } + { // All good. + scoped_ptr<base::DictionaryValue> json(ParseJson(kValidTokenResponse)); + std::string at; + EXPECT_TRUE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at)); + EXPECT_EQ("at1", at); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ParseIssueAdviceResponse) { + { // Description missing. + scoped_ptr<base::DictionaryValue> json( + ParseJson(kIssueAdviceResponseNoDescription)); + IssueAdviceInfo ia; + EXPECT_FALSE(OAuth2MintTokenFlow::ParseIssueAdviceResponse( + json.get(), &ia)); + EXPECT_TRUE(ia.empty()); + } + { // Detail missing. + scoped_ptr<base::DictionaryValue> json( + ParseJson(kIssueAdviceResponseNoDetail)); + IssueAdviceInfo ia; + EXPECT_FALSE(OAuth2MintTokenFlow::ParseIssueAdviceResponse( + json.get(), &ia)); + EXPECT_TRUE(ia.empty()); + } + { // All good. + scoped_ptr<base::DictionaryValue> json( + ParseJson(kValidIssueAdviceResponse)); + IssueAdviceInfo ia; + EXPECT_TRUE(OAuth2MintTokenFlow::ParseIssueAdviceResponse( + json.get(), &ia)); + IssueAdviceInfo ia_expected(CreateIssueAdvice()); + EXPECT_EQ(ia_expected, ia); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallSuccess) { + { // No body. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(""); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Bad json. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString("foo"); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: no access token. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kTokenResponseNoAccessToken); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: good token response. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kValidTokenResponse); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenSuccess("at1")); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: no description. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kIssueAdviceResponseNoDescription); + CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: no detail. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kIssueAdviceResponseNoDetail); + CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: good issue advice response. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kValidIssueAdviceResponse); + CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE); + IssueAdviceInfo ia(CreateIssueAdvice()); + EXPECT_CALL(delegate_, OnIssueAdviceSuccess(ia)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallFailure) { + { // Null delegate should work fine. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.set_status(URLRequestStatus(URLRequestStatus::FAILED, 101)); + CreateFlow(NULL, OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + flow_->ProcessApiCallFailure(&url_fetcher); + } + + { // Non-null delegate. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.set_status(URLRequestStatus(URLRequestStatus::FAILED, 101)); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallFailure(&url_fetcher); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ProcessMintAccessTokenFailure) { + { // Null delegate should work fine. + GoogleServiceAuthError error( + GoogleServiceAuthError::FromConnectionError(101)); + CreateFlow(NULL, OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + flow_->ProcessMintAccessTokenFailure(error); + } + + { // Non-null delegate. + GoogleServiceAuthError error( + GoogleServiceAuthError::FromConnectionError(101)); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(error)); + flow_->ProcessMintAccessTokenFailure(error); + } +} diff --git a/google_apis/gaia/oauth2_revocation_consumer.h b/google_apis/gaia/oauth2_revocation_consumer.h new file mode 100644 index 0000000..2004fec --- /dev/null +++ b/google_apis/gaia/oauth2_revocation_consumer.h @@ -0,0 +1,22 @@ +// Copyright (c) 2011 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH2_REVOCATION_CONSUMER_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_REVOCATION_CONSUMER_H_ + +#include <string> + +class GoogleServiceAuthError; + +// An interface that defines the callbacks for consumers to which +// OAuth2RevocationFetcher can return results. +class OAuth2RevocationConsumer { + public: + virtual ~OAuth2RevocationConsumer() {} + + virtual void OnRevocationSuccess() {} + virtual void OnRevocationFailure(const GoogleServiceAuthError& error) {} +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_REVOCATION_CONSUMER_H_ diff --git a/google_apis/gaia/oauth2_revocation_fetcher.cc b/google_apis/gaia/oauth2_revocation_fetcher.cc new file mode 100644 index 0000000..2a4bae5 --- /dev/null +++ b/google_apis/gaia/oauth2_revocation_fetcher.cc @@ -0,0 +1,165 @@ +// 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 "google_apis/gaia/oauth2_revocation_fetcher.h" + +#include <algorithm> +#include <string> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using net::ResponseCookies; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { +static const char kOAuth2RevokeTokenURL[] = + "https://www.googleapis.com/oauth2/v2/RevokeToken"; + +static const char kAuthorizationHeaderFormat[] = + "Authorization: Bearer %s"; + +static const char kRevocationBodyFormat[] = + "client_id=%s&origin=%s"; + +static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) { + CHECK(!status.is_success()); + if (status.status() == URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } +} + +static URLFetcher* CreateFetcher(URLRequestContextGetter* getter, + const GURL& url, + const std::string& header, + const std::string& body, + URLFetcherDelegate* delegate) { + bool empty_body = body.empty(); + URLFetcher* result = net::URLFetcher::Create( + 0, url, + empty_body ? URLFetcher::GET : URLFetcher::POST, + delegate); + + result->SetRequestContext(getter); + result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + if (!header.empty()) + result->SetExtraRequestHeaders(header); + + if (!empty_body) + result->SetUploadData("application/x-www-form-urlencoded", body); + + return result; +} +} // namespace + +OAuth2RevocationFetcher::OAuth2RevocationFetcher( + OAuth2RevocationConsumer* consumer, + URLRequestContextGetter* getter) + : consumer_(consumer), + getter_(getter), + state_(INITIAL) { } + +OAuth2RevocationFetcher::~OAuth2RevocationFetcher() { } + +void OAuth2RevocationFetcher::CancelRequest() { + fetcher_.reset(); +} + +void OAuth2RevocationFetcher::Start(const std::string& access_token, + const std::string& client_id, + const std::string& origin) { + access_token_ = access_token; + client_id_ = client_id; + origin_ = origin; + StartRevocation(); +} + +void OAuth2RevocationFetcher::StartRevocation() { + CHECK_EQ(INITIAL, state_); + state_ = REVOCATION_STARTED; + fetcher_.reset(CreateFetcher( + getter_, + MakeRevocationUrl(), + MakeRevocationHeader(access_token_), + MakeRevocationBody(client_id_, origin_), + this)); + fetcher_->Start(); // OnURLFetchComplete will be called. +} + +void OAuth2RevocationFetcher::EndRevocation(const net::URLFetcher* source) { + CHECK_EQ(REVOCATION_STARTED, state_); + state_ = REVOCATION_DONE; + + URLRequestStatus status = source->GetStatus(); + if (!status.is_success()) { + OnRevocationFailure(CreateAuthError(status)); + return; + } + + if (source->GetResponseCode() != net::HTTP_NO_CONTENT) { + OnRevocationFailure(GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + return; + } + + OnRevocationSuccess(); +} + +void OAuth2RevocationFetcher::OnRevocationSuccess() { + consumer_->OnRevocationSuccess(); +} + +void OAuth2RevocationFetcher::OnRevocationFailure( + const GoogleServiceAuthError& error) { + state_ = ERROR_STATE; + consumer_->OnRevocationFailure(error); +} + +void OAuth2RevocationFetcher::OnURLFetchComplete( + const net::URLFetcher* source) { + CHECK(source); + EndRevocation(source); +} + +// static +GURL OAuth2RevocationFetcher::MakeRevocationUrl() { + return GURL(kOAuth2RevokeTokenURL); +} + +// static +std::string OAuth2RevocationFetcher::MakeRevocationHeader( + const std::string& access_token) { + return StringPrintf(kAuthorizationHeaderFormat, access_token.c_str()); +} + +// static +std::string OAuth2RevocationFetcher::MakeRevocationBody( + const std::string& client_id, + const std::string& origin) { + std::string enc_client_id = net::EscapeUrlEncodedData(client_id, true); + std::string enc_origin = net::EscapeUrlEncodedData(origin, true); + return StringPrintf( + kRevocationBodyFormat, + enc_client_id.c_str(), + enc_origin.c_str()); +} diff --git a/google_apis/gaia/oauth2_revocation_fetcher.h b/google_apis/gaia/oauth2_revocation_fetcher.h new file mode 100644 index 0000000..a87ee78 --- /dev/null +++ b/google_apis/gaia/oauth2_revocation_fetcher.h @@ -0,0 +1,94 @@ +// 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH2_REVOCATION_FETCHER_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_REVOCATION_FETCHER_H_ + +#include <string> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/oauth2_revocation_consumer.h" +#include "googleurl/src/gurl.h" +#include "net/url_request/url_fetcher_delegate.h" + +class OAuth2RevocationFetcherTest; + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +class URLRequestStatus; +} + +// Abstracts the details to perform OAuth2 grant revocation. +// +// This class should be used on a single thread, but it can be whichever thread +// that you like. +// Also, do not reuse the same instance. Once Start() is called, the instance +// should not be reused. +// +// Usage: +// * Create an instance with a consumer. +// * Call Start() +// * The consumer passed in the constructor will be called on the same +// thread Start was called with the results. +// +// This class can handle one request at a time. To parallelize requests, +// create multiple instances. +class OAuth2RevocationFetcher : public net::URLFetcherDelegate { + public: + OAuth2RevocationFetcher(OAuth2RevocationConsumer* consumer, + net::URLRequestContextGetter* getter); + virtual ~OAuth2RevocationFetcher(); + + // Starts the flow with the given parameters. + // |access_token| should be an OAuth2 login scoped access token. + void Start(const std::string& access_token, + const std::string& client_id, + const std::string& origin); + + void CancelRequest(); + + // Implementation of net::URLFetcherDelegate + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + enum State { + INITIAL, + REVOCATION_STARTED, + REVOCATION_DONE, + ERROR_STATE, + }; + + // Helper methods for the flow. + void StartRevocation(); + void EndRevocation(const net::URLFetcher* source); + + // Helper mehtods for reporting back results. + void OnRevocationSuccess(); + void OnRevocationFailure(const GoogleServiceAuthError& error); + + // Other helpers. + static GURL MakeRevocationUrl(); + static std::string MakeRevocationHeader(const std::string& access_token); + static std::string MakeRevocationBody(const std::string& client_id, + const std::string& origin); + + // State that is set during construction. + OAuth2RevocationConsumer* const consumer_; + net::URLRequestContextGetter* const getter_; + State state_; + + // While a fetch is in progress. + scoped_ptr<net::URLFetcher> fetcher_; + std::string access_token_; + std::string client_id_; + std::string origin_; + + friend class OAuth2RevocationFetcherTest; + + DISALLOW_COPY_AND_ASSIGN(OAuth2RevocationFetcher); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_REVOCATION_FETCHER_H_ diff --git a/google_apis/gaia/oauth2_revocation_fetcher_unittest.cc b/google_apis/gaia/oauth2_revocation_fetcher_unittest.cc new file mode 100644 index 0000000..17f53ad --- /dev/null +++ b/google_apis/gaia/oauth2_revocation_fetcher_unittest.cc @@ -0,0 +1,122 @@ +// 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. +// +// A complete set of unit tests for OAuth2RevocationFetcher. + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/test_browser_thread.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_revocation_consumer.h" +#include "google_apis/gaia/oauth2_revocation_fetcher.h" +#include "googleurl/src/gurl.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_fetcher_factory.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_status.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using content::BrowserThread; +using net::ResponseCookies; +using net::ScopedURLFetcherFactory; +using net::TestURLFetcher; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLFetcherFactory; +using net::URLRequestStatus; +using testing::_; +using testing::Return; + +namespace { + +class MockUrlFetcherFactory : public ScopedURLFetcherFactory, + public URLFetcherFactory { +public: + MockUrlFetcherFactory() + : ScopedURLFetcherFactory(ALLOW_THIS_IN_INITIALIZER_LIST(this)) { + } + virtual ~MockUrlFetcherFactory() {} + + MOCK_METHOD4( + CreateURLFetcher, + URLFetcher* (int id, + const GURL& url, + URLFetcher::RequestType request_type, + URLFetcherDelegate* d)); +}; + +class MockOAuth2RevocationConsumer : public OAuth2RevocationConsumer { + public: + MockOAuth2RevocationConsumer() {} + ~MockOAuth2RevocationConsumer() {} + + MOCK_METHOD0(OnRevocationSuccess, void()); + MOCK_METHOD1(OnRevocationFailure, + void(const GoogleServiceAuthError& error)); +}; + +} // namespace + +class OAuth2RevocationFetcherTest : public testing::Test { + public: + OAuth2RevocationFetcherTest() + : ui_thread_(BrowserThread::UI, &message_loop_), + fetcher_(&consumer_, profile_.GetRequestContext()) { + } + + virtual ~OAuth2RevocationFetcherTest() { } + + virtual TestURLFetcher* SetupRevocation( + bool fetch_succeeds, int response_code) { + GURL url = OAuth2RevocationFetcher::MakeRevocationUrl(); + TestURLFetcher* url_fetcher = new TestURLFetcher(0, url, &fetcher_); + URLRequestStatus::Status status = + fetch_succeeds ? URLRequestStatus::SUCCESS : URLRequestStatus::FAILED; + url_fetcher->set_status(URLRequestStatus(status, 0)); + + if (response_code != 0) + url_fetcher->set_response_code(response_code); + + EXPECT_CALL(factory_, CreateURLFetcher(_, url, _, _)) + .WillOnce(Return(url_fetcher)); + return url_fetcher; + } + + protected: + MessageLoop message_loop_; + content::TestBrowserThread ui_thread_; + MockUrlFetcherFactory factory_; + MockOAuth2RevocationConsumer consumer_; + TestingProfile profile_; + OAuth2RevocationFetcher fetcher_; +}; + +TEST_F(OAuth2RevocationFetcherTest, RequestFailure) { + TestURLFetcher* url_fetcher = SetupRevocation(false, 0); + EXPECT_CALL(consumer_, OnRevocationFailure(_)).Times(1); + fetcher_.Start("access_token", "client_id", "origin"); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2RevocationFetcherTest, ResponseCodeFailure) { + TestURLFetcher* url_fetcher = SetupRevocation(true, net::HTTP_FORBIDDEN); + EXPECT_CALL(consumer_, OnRevocationFailure(_)).Times(1); + fetcher_.Start("access_token", "client_id", "origin"); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2RevocationFetcherTest, Success) { + TestURLFetcher* url_fetcher = SetupRevocation(true, net::HTTP_NO_CONTENT); + EXPECT_CALL(consumer_, OnRevocationSuccess()).Times(1); + fetcher_.Start("access_token", "client_id", "origin"); + fetcher_.OnURLFetchComplete(url_fetcher); +} diff --git a/google_apis/gaia/oauth_request_signer.cc b/google_apis/gaia/oauth_request_signer.cc new file mode 100644 index 0000000..7ba947b --- /dev/null +++ b/google_apis/gaia/oauth_request_signer.cc @@ -0,0 +1,458 @@ +// 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 "google_apis/gaia/oauth_request_signer.h" + +#include <cctype> +#include <cstddef> +#include <cstdlib> +#include <cstring> +#include <ctime> +#include <map> +#include <string> + +#include "base/base64.h" +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/rand_util.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/time.h" +#include "crypto/hmac.h" +#include "googleurl/src/gurl.h" + +namespace { + +static const int kHexBase = 16; +static char kHexDigits[] = "0123456789ABCDEF"; +static const size_t kHmacDigestLength = 20; +static const int kMaxNonceLength = 30; +static const int kMinNonceLength = 15; + +static const char kOAuthConsumerKeyLabel[] = "oauth_consumer_key"; +static const char kOAuthConsumerSecretLabel[] = "oauth_consumer_secret"; +static const char kOAuthNonceCharacters[] = + "abcdefghijklmnopqrstuvwyz" + "ABCDEFGHIJKLMNOPQRSTUVWYZ" + "0123456789_"; +static const char kOAuthNonceLabel[] = "oauth_nonce"; +static const char kOAuthSignatureLabel[] = "oauth_signature"; +static const char kOAuthSignatureMethodLabel[] = "oauth_signature_method"; +static const char kOAuthTimestampLabel[] = "oauth_timestamp"; +static const char kOAuthTokenLabel[] = "oauth_token"; +static const char kOAuthTokenSecretLabel[] = "oauth_token_secret"; +static const char kOAuthVersion[] = "1.0"; +static const char kOAuthVersionLabel[] = "oauth_version"; + +enum ParseQueryState { + START_STATE, + KEYWORD_STATE, + VALUE_STATE, +}; + +const std::string HttpMethodName(OAuthRequestSigner::HttpMethod method) { + switch (method) { + case OAuthRequestSigner::GET_METHOD: + return "GET"; + case OAuthRequestSigner::POST_METHOD: + return "POST"; + } + NOTREACHED(); + return *(new std::string()); +} + +const std::string SignatureMethodName( + OAuthRequestSigner::SignatureMethod method) { + switch (method) { + case OAuthRequestSigner::HMAC_SHA1_SIGNATURE: + return "HMAC-SHA1"; + case OAuthRequestSigner::RSA_SHA1_SIGNATURE: + return "RSA-SHA1"; + case OAuthRequestSigner::PLAINTEXT_SIGNATURE: + return "PLAINTEXT"; + } + NOTREACHED(); + return *(new std::string()); +} + +std::string BuildBaseString(const GURL& request_base_url, + OAuthRequestSigner::HttpMethod http_method, + const std::string& base_parameters) { + return StringPrintf("%s&%s&%s", + HttpMethodName(http_method).c_str(), + OAuthRequestSigner::Encode( + request_base_url.spec()).c_str(), + OAuthRequestSigner::Encode( + base_parameters).c_str()); +} + +std::string BuildBaseStringParameters( + const OAuthRequestSigner::Parameters& parameters) { + std::string result = ""; + OAuthRequestSigner::Parameters::const_iterator cursor; + OAuthRequestSigner::Parameters::const_iterator limit; + bool first = true; + for (cursor = parameters.begin(), limit = parameters.end(); + cursor != limit; + ++cursor) { + if (first) + first = false; + else + result += '&'; + result += OAuthRequestSigner::Encode(cursor->first); + result += '='; + result += OAuthRequestSigner::Encode(cursor->second); + } + return result; +} + +std::string GenerateNonce() { + char result[kMaxNonceLength + 1]; + int length = base::RandUint64() % (kMaxNonceLength - kMinNonceLength + 1) + + kMinNonceLength; + result[length] = '\0'; + for (int index = 0; index < length; ++index) + result[index] = kOAuthNonceCharacters[ + base::RandUint64() % (sizeof(kOAuthNonceCharacters) - 1)]; + return result; +} + +std::string GenerateTimestamp() { + return base::StringPrintf( + "%" PRId64, + (base::Time::NowFromSystemTime() - base::Time::UnixEpoch()).InSeconds()); +} + +// Creates a string-to-string, keyword-value map from a parameter/query string +// that uses ampersand (&) to seperate paris and equals (=) to seperate +// keyword from value. +bool ParseQuery(const std::string& query, + OAuthRequestSigner::Parameters* parameters_result) { + std::string::const_iterator cursor; + std::string keyword; + std::string::const_iterator limit; + OAuthRequestSigner::Parameters parameters; + ParseQueryState state; + std::string value; + + state = START_STATE; + for (cursor = query.begin(), limit = query.end(); + cursor != limit; + ++cursor) { + char character = *cursor; + switch (state) { + case KEYWORD_STATE: + switch (character) { + case '&': + parameters[keyword] = value; + keyword = ""; + value = ""; + state = START_STATE; + break; + case '=': + state = VALUE_STATE; + break; + default: + keyword += character; + } + break; + case START_STATE: + switch (character) { + case '&': // Intentionally falling through + case '=': + return false; + default: + keyword += character; + state = KEYWORD_STATE; + } + break; + case VALUE_STATE: + switch (character) { + case '=': + return false; + case '&': + parameters[keyword] = value; + keyword = ""; + value = ""; + state = START_STATE; + break; + default: + value += character; + } + break; + } + } + switch (state) { + case START_STATE: + break; + case KEYWORD_STATE: // Intentionally falling through + case VALUE_STATE: + parameters[keyword] = value; + break; + default: + NOTREACHED(); + } + *parameters_result = parameters; + return true; +} + +// Creates the value for the oauth_signature parameter when the +// oauth_signature_method is HMAC-SHA1. +bool SignHmacSha1(const std::string& text, + const std::string& key, + std::string* signature_return) { + crypto::HMAC hmac(crypto::HMAC::SHA1); + DCHECK(hmac.DigestLength() == kHmacDigestLength); + unsigned char digest[kHmacDigestLength]; + bool result = hmac.Init(key) && + hmac.Sign(text, digest, kHmacDigestLength) && + base::Base64Encode(std::string(reinterpret_cast<const char*>(digest), + kHmacDigestLength), + signature_return); + return result; +} + +// Creates the value for the oauth_signature parameter when the +// oauth_signature_method is PLAINTEXT. +// +// Not yet implemented, and might never be. +bool SignPlaintext(const std::string& text, + const std::string& key, + std::string* result) { + NOTIMPLEMENTED(); + return false; +} + +// Creates the value for the oauth_signature parameter when the +// oauth_signature_method is RSA-SHA1. +// +// Not yet implemented, and might never be. +bool SignRsaSha1(const std::string& text, + const std::string& key, + std::string* result) { + NOTIMPLEMENTED(); + return false; +} + +// Adds parameters that are required by OAuth added as needed to |parameters|. +void PrepareParameters(OAuthRequestSigner::Parameters* parameters, + OAuthRequestSigner::SignatureMethod signature_method, + OAuthRequestSigner::HttpMethod http_method, + const std::string& consumer_key, + const std::string& token_key) { + if (parameters->find(kOAuthNonceLabel) == parameters->end()) + (*parameters)[kOAuthNonceLabel] = GenerateNonce(); + + if (parameters->find(kOAuthTimestampLabel) == parameters->end()) + (*parameters)[kOAuthTimestampLabel] = GenerateTimestamp(); + + (*parameters)[kOAuthConsumerKeyLabel] = consumer_key; + (*parameters)[kOAuthSignatureMethodLabel] = + SignatureMethodName(signature_method); + (*parameters)[kOAuthTokenLabel] = token_key; + (*parameters)[kOAuthVersionLabel] = kOAuthVersion; +} + +// Implements shared signing logic, generating the signature and storing it in +// |parameters|. Returns true if the signature has been generated succesfully. +bool SignParameters(const GURL& request_base_url, + OAuthRequestSigner::SignatureMethod signature_method, + OAuthRequestSigner::HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + OAuthRequestSigner::Parameters* parameters) { + DCHECK(request_base_url.is_valid()); + PrepareParameters(parameters, signature_method, http_method, + consumer_key, token_key); + std::string base_parameters = BuildBaseStringParameters(*parameters); + std::string base = BuildBaseString(request_base_url, http_method, + base_parameters); + std::string key = consumer_secret + '&' + token_secret; + bool is_signed = false; + std::string signature; + switch (signature_method) { + case OAuthRequestSigner::HMAC_SHA1_SIGNATURE: + is_signed = SignHmacSha1(base, key, &signature); + break; + case OAuthRequestSigner::RSA_SHA1_SIGNATURE: + is_signed = SignRsaSha1(base, key, &signature); + break; + case OAuthRequestSigner::PLAINTEXT_SIGNATURE: + is_signed = SignPlaintext(base, key, &signature); + break; + default: + NOTREACHED(); + } + if (is_signed) + (*parameters)[kOAuthSignatureLabel] = signature; + return is_signed; +} + + +} // namespace + +// static +bool OAuthRequestSigner::Decode(const std::string& text, + std::string* decoded_text) { + std::string accumulator = ""; + std::string::const_iterator cursor; + std::string::const_iterator limit; + for (limit = text.end(), cursor = text.begin(); cursor != limit; ++cursor) { + char character = *cursor; + if (character == '%') { + ++cursor; + if (cursor == limit) + return false; + char* first = strchr(kHexDigits, *cursor); + if (!first) + return false; + int high = first - kHexDigits; + DCHECK(high >= 0 && high < kHexBase); + + ++cursor; + if (cursor == limit) + return false; + char* second = strchr(kHexDigits, *cursor); + if (!second) + return false; + int low = second - kHexDigits; + DCHECK(low >= 0 || low < kHexBase); + + char decoded = static_cast<char>(high * kHexBase + low); + DCHECK(!(IsAsciiAlpha(decoded) || IsAsciiDigit(decoded))); + DCHECK(!(decoded && strchr("-._~", decoded))); + accumulator += decoded; + } else { + accumulator += character; + } + } + *decoded_text = accumulator; + return true; +} + +// static +std::string OAuthRequestSigner::Encode(const std::string& text) { + std::string result = ""; + std::string::const_iterator cursor; + std::string::const_iterator limit; + for (limit = text.end(), cursor = text.begin(); cursor != limit; ++cursor) { + char character = *cursor; + if (IsAsciiAlpha(character) || IsAsciiDigit(character)) { + result += character; + } else { + switch (character) { + case '-': + case '.': + case '_': + case '~': + result += character; + break; + default: + unsigned char byte = static_cast<unsigned char>(character); + result = result + '%' + kHexDigits[byte / kHexBase] + + kHexDigits[byte % kHexBase]; + } + } + } + return result; +} + +// static +bool OAuthRequestSigner::ParseAndSign(const GURL& request_url_with_parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* result) { + DCHECK(request_url_with_parameters.is_valid()); + Parameters parameters; + if (request_url_with_parameters.has_query()) { + const std::string& query = request_url_with_parameters.query(); + if (!query.empty()) { + if (!ParseQuery(query, ¶meters)) + return false; + } + } + std::string spec = request_url_with_parameters.spec(); + std::string url_without_parameters = spec; + std::string::size_type question = spec.find("?"); + if (question != std::string::npos) + url_without_parameters = spec.substr(0,question); + return SignURL(GURL(url_without_parameters), parameters, signature_method, + http_method, consumer_key, consumer_secret, token_key, + token_secret, result); +} + +// static +bool OAuthRequestSigner::SignURL( + const GURL& request_base_url, + const Parameters& request_parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_text_return) { + DCHECK(request_base_url.is_valid()); + Parameters parameters(request_parameters); + bool is_signed = SignParameters(request_base_url, signature_method, + http_method, consumer_key, consumer_secret, + token_key, token_secret, ¶meters); + if (is_signed) { + std::string signed_text; + switch (http_method) { + case GET_METHOD: + signed_text = request_base_url.spec() + '?'; + // Intentionally falling through + case POST_METHOD: + signed_text += BuildBaseStringParameters(parameters); + break; + default: + NOTREACHED(); + } + *signed_text_return = signed_text; + } + return is_signed; +} + +// static +bool OAuthRequestSigner::SignAuthHeader( + const GURL& request_base_url, + const Parameters& request_parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_text_return) { + DCHECK(request_base_url.is_valid()); + Parameters parameters(request_parameters); + bool is_signed = SignParameters(request_base_url, signature_method, + http_method, consumer_key, consumer_secret, + token_key, token_secret, ¶meters); + if (is_signed) { + std::string signed_text = "OAuth "; + bool first = true; + for (Parameters::const_iterator param = parameters.begin(); + param != parameters.end(); + ++param) { + if (first) + first = false; + else + signed_text += ", "; + signed_text += + StringPrintf("%s=\"%s\"", + OAuthRequestSigner::Encode(param->first).c_str(), + OAuthRequestSigner::Encode(param->second).c_str()); + } + *signed_text_return = signed_text; + } + return is_signed; +} diff --git a/google_apis/gaia/oauth_request_signer.h b/google_apis/gaia/oauth_request_signer.h new file mode 100644 index 0000000..3b91d4d --- /dev/null +++ b/google_apis/gaia/oauth_request_signer.h @@ -0,0 +1,100 @@ +// Copyright (c) 2011 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. + +#ifndef GOOGLE_APIS_GAIA_OAUTH_REQUEST_SIGNER_H_ +#define GOOGLE_APIS_GAIA_OAUTH_REQUEST_SIGNER_H_ + +#include <map> +#include <string> + +#include "base/basictypes.h" + +class GURL; + +// Implements the OAuth request signing process as described here: +// http://oauth.net/core/1.0/#signing_process +// +// NOTE: Currently the only supported SignatureMethod is HMAC_SHA1_SIGNATURE +class OAuthRequestSigner { + public: + enum SignatureMethod { + HMAC_SHA1_SIGNATURE, + RSA_SHA1_SIGNATURE, + PLAINTEXT_SIGNATURE + }; + + enum HttpMethod { + GET_METHOD, + POST_METHOD + }; + + typedef std::map<std::string,std::string> Parameters; + + // Percent encoding and decoding for OAuth. + // + // The form of percent encoding used for OAuth request signing is very + // specific and strict. See http://oauth.net/core/1.0/#encoding_parameters. + // This definition is considered the current standard as of January 2005. + // While as of July 2011 many systems to do not comply, any valid OAuth + // implementation must comply. + // + // Any character which is in the "unreserved set" MUST NOT be encoded. + // All other characters MUST be encoded. + // + // The unreserved set is comprised of the alphanumeric characters and these + // others: + // - minus (-) + // - period (.) + // - underscore (_) + // - tilde (~) + static bool Decode(const std::string& text, std::string* decoded_text); + static std::string Encode(const std::string& text); + + // Signs a request specified as URL string, complete with parameters. + // + // If HttpMethod is GET_METHOD, the signed result is the full URL, otherwise + // it is the request parameters, including the oauth_signature field. + static bool ParseAndSign(const GURL& request_url_with_parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_result); + + // Signs a request specified as the combination of a base URL string, with + // parameters included in a separate map data structure. NOTE: The base URL + // string must not contain a question mark (?) character. If it does, + // you can use ParseAndSign() instead. + // + // If HttpMethod is GET_METHOD, the signed result is the full URL, otherwise + // it is the request parameters, including the oauth_signature field. + static bool SignURL(const GURL& request_base_url, + const Parameters& parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_result); + + // Similar to SignURL(), but the returned string is not a URL, but the payload + // to for an HTTP Authorization header. + static bool SignAuthHeader(const GURL& request_base_url, + const Parameters& parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_result); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(OAuthRequestSigner); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH_REQUEST_SIGNER_H_ diff --git a/google_apis/gaia/oauth_request_signer_unittest.cc b/google_apis/gaia/oauth_request_signer_unittest.cc new file mode 100644 index 0000000..5cf2dc2 --- /dev/null +++ b/google_apis/gaia/oauth_request_signer_unittest.cc @@ -0,0 +1,323 @@ +// Copyright (c) 2011 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 "google_apis/gaia/oauth_request_signer.h" + +#include "googleurl/src/gurl.h" +#include "testing/gtest/include/gtest/gtest.h" + +// This value is used to seed the PRNG at the beginning of a sequence of +// operations to produce a repeatable sequence. +#define RANDOM_SEED (0x69E3C47D) + +TEST(OAuthRequestSignerTest, Encode) { + ASSERT_EQ(OAuthRequestSigner::Encode("ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-._~"), + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-._~"); + ASSERT_EQ(OAuthRequestSigner::Encode( + "https://accounts.google.com/OAuthLogin"), + "https%3A%2F%2Faccounts.google.com%2FOAuthLogin"); + ASSERT_EQ(OAuthRequestSigner::Encode("%"), "%25"); + ASSERT_EQ(OAuthRequestSigner::Encode("%25"), "%2525"); + ASSERT_EQ(OAuthRequestSigner::Encode( + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed " + "do eiusmod tempor incididunt ut labore et dolore magna " + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation " + "ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis " + "aute irure dolor in reprehenderit in voluptate velit esse " + "cillum dolore eu fugiat nulla pariatur. Excepteur sint " + "occaecat cupidatat non proident, sunt in culpa qui officia " + "deserunt mollit anim id est laborum."), + "Lorem%20ipsum%20dolor%20sit%20amet%2C%20consectetur%20" + "adipisicing%20elit%2C%20sed%20do%20eiusmod%20tempor%20" + "incididunt%20ut%20labore%20et%20dolore%20magna%20aliqua.%20Ut%20" + "enim%20ad%20minim%20veniam%2C%20quis%20nostrud%20exercitation%20" + "ullamco%20laboris%20nisi%20ut%20aliquip%20ex%20ea%20commodo%20" + "consequat.%20Duis%20aute%20irure%20dolor%20in%20reprehenderit%20" + "in%20voluptate%20velit%20esse%20cillum%20dolore%20eu%20fugiat%20" + "nulla%20pariatur.%20Excepteur%20sint%20occaecat%20cupidatat%20" + "non%20proident%2C%20sunt%20in%20culpa%20qui%20officia%20" + "deserunt%20mollit%20anim%20id%20est%20laborum."); + ASSERT_EQ(OAuthRequestSigner::Encode("!5}&QF~0R-Ecy[?2Cig>6g=;hH!\\Ju4K%UK;"), + "%215%7D%26QF~0R-Ecy%5B%3F2Cig%3E6g%3D%3BhH%21%5CJu4K%25UK%3B"); + ASSERT_EQ(OAuthRequestSigner::Encode("1UgHf(r)SkMRS`fRZ/8PsTcXT0:\\<9I=6{|:"), + "1UgHf%28r%29SkMRS%60fRZ%2F8PsTcXT0%3A%5C%3C9I%3D6%7B%7C%3A"); + ASSERT_EQ(OAuthRequestSigner::Encode("|<XIy1?o`r\"RuGSX#!:MeP&RLZQM@:\\';2X"), + "%7C%3CXIy1%3Fo%60r%22RuGSX%23%21%3AMeP%26RLZQM%40%3A%5C%27%3B2X"); + ASSERT_EQ(OAuthRequestSigner::Encode("#a@A>ZtcQ/yb.~^Q_]daRT?ffK>@A:afWuZL"), + "%23a%40A%3EZtcQ%2Fyb.~%5EQ_%5DdaRT%3FffK%3E%40A%3AafWuZL"); +} + +TEST(OAuthRequestSignerTest, DecodeEncoded) { + srand(RANDOM_SEED); + static const int kIterations = 500; + static const int kLengthLimit = 500; + for (int iteration = 0; iteration < kIterations; ++iteration) { + std::string text; + int length = rand() % kLengthLimit; + for (int position = 0; position < length; ++position) { + text += static_cast<char>(rand() % 256); + } + std::string encoded = OAuthRequestSigner::Encode(text); + std::string decoded; + ASSERT_TRUE(OAuthRequestSigner::Decode(encoded, &decoded)); + ASSERT_EQ(decoded, text); + } +} + +TEST(OAuthRequestSignerTest, SignGet1) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken"); + OAuthRequestSigner::Parameters parameters; + parameters["scope"] = "https://accounts.google.com/OAuthLogin"; + parameters["oauth_nonce"] = "2oiE_aHdk5qRTz0L9C8Lq0g"; + parameters["xaouth_display_name"] = "Chromium"; + parameters["oauth_timestamp"] = "1308152953"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignURL( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "johndoe", // oauth_consumer_key + "53cR3t", // consumer secret + "4/VGY0MsQadcmO8VnCv9gnhoEooq1v", // oauth_token + "c5e0531ff55dfbb4054e", // token secret + &signed_text)); + ASSERT_EQ("https://www.google.com/accounts/o8/GetOAuthToken" + "?oauth_consumer_key=johndoe" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&oauth_signature=PFqDTaiyey1UObcvOyI4Ng2HXW0%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308152953" + "&oauth_token=4%2FVGY0MsQadcmO8VnCv9gnhoEooq1v" + "&oauth_version=1.0" + "&scope=https%3A%2F%2Faccounts.google.com%2FOAuthLogin" + "&xaouth_display_name=Chromium", + signed_text); +} + +TEST(OAuthRequestSignerTest, SignGet2) { + GURL request_url("https://accounts.google.com/OAuthGetAccessToken"); + OAuthRequestSigner::Parameters parameters; + parameters["oauth_timestamp"] = "1308147831"; + parameters["oauth_nonce"] = "4d4hZW9DygWQujP2tz06UN"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignURL( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + "", // token secret + &signed_text)); + ASSERT_EQ(signed_text, + "https://accounts.google.com/OAuthGetAccessToken" + "?oauth_consumer_key=anonymous" + "&oauth_nonce=4d4hZW9DygWQujP2tz06UN" + "&oauth_signature=YiJv%2BEOWsvCDCi13%2FhQBFrr0J7c%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308147831" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0"); +} + +TEST(OAuthRequestSignerTest, ParseAndSignGet1) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken" + "?scope=https://accounts.google.com/OAuthLogin" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&xaouth_display_name=Chromium" + "&oauth_timestamp=1308152953"); + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::ParseAndSign( + request_url, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + "", // token secret + &signed_text)); + ASSERT_EQ("https://www.google.com/accounts/o8/GetOAuthToken" + "?oauth_consumer_key=anonymous" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&oauth_signature=PH7KP6cP%2BzZ1SJ6WGqBgXwQP9Mc%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308152953" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0" + "&scope=https%3A%2F%2Faccounts.google.com%2FOAuthLogin" + "&xaouth_display_name=Chromium", + signed_text); +} + +TEST(OAuthRequestSignerTest, ParseAndSignGet2) { + GURL request_url("https://accounts.google.com/OAuthGetAccessToken" + "?oauth_timestamp=1308147831" + "&oauth_nonce=4d4hZW9DygWQujP2tz06UN"); + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::ParseAndSign( + request_url, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + "", // token secret + &signed_text)); + ASSERT_EQ(signed_text, + "https://accounts.google.com/OAuthGetAccessToken" + "?oauth_consumer_key=anonymous" + "&oauth_nonce=4d4hZW9DygWQujP2tz06UN" + "&oauth_signature=YiJv%2BEOWsvCDCi13%2FhQBFrr0J7c%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308147831" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0"); +} + +TEST(OAuthRequestSignerTest, SignPost1) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken"); + OAuthRequestSigner::Parameters parameters; + parameters["scope"] = "https://accounts.google.com/OAuthLogin"; + parameters["oauth_nonce"] = "2oiE_aHdk5qRTz0L9C8Lq0g"; + parameters["xaouth_display_name"] = "Chromium"; + parameters["oauth_timestamp"] = "1308152953"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignURL( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::POST_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/X8x0r7bHif_VNCLjUMutxGkzo13d", // oauth_token + "b7120598d47594bd3522", // token secret + &signed_text)); + ASSERT_EQ("oauth_consumer_key=anonymous" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&oauth_signature=vVlfv6dnV2%2Fx7TozS0Gf83zS2%2BQ%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308152953" + "&oauth_token=4%2FX8x0r7bHif_VNCLjUMutxGkzo13d" + "&oauth_version=1.0" + "&scope=https%3A%2F%2Faccounts.google.com%2FOAuthLogin" + "&xaouth_display_name=Chromium", + signed_text); +} + +TEST(OAuthRequestSignerTest, SignPost2) { + GURL request_url("https://accounts.google.com/OAuthGetAccessToken"); + OAuthRequestSigner::Parameters parameters; + parameters["oauth_timestamp"] = "1234567890"; + parameters["oauth_nonce"] = "17171717171717171"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignURL( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::POST_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + "", // token secret + &signed_text)); + ASSERT_EQ(signed_text, + "oauth_consumer_key=anonymous" + "&oauth_nonce=17171717171717171" + "&oauth_signature=tPX2XqKQICWzopZ80CFGX%2F53DLo%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1234567890" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0"); +} + +TEST(OAuthRequestSignerTest, ParseAndSignPost1) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken" + "?scope=https://accounts.google.com/OAuthLogin" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&xaouth_display_name=Chromium" + "&oauth_timestamp=1308152953"); + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::ParseAndSign( + request_url, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::POST_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/X8x0r7bHif_VNCLjUMutxGkzo13d", // oauth_token + "b7120598d47594bd3522", // token secret + &signed_text)); + ASSERT_EQ("oauth_consumer_key=anonymous" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&oauth_signature=vVlfv6dnV2%2Fx7TozS0Gf83zS2%2BQ%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308152953" + "&oauth_token=4%2FX8x0r7bHif_VNCLjUMutxGkzo13d" + "&oauth_version=1.0" + "&scope=https%3A%2F%2Faccounts.google.com%2FOAuthLogin" + "&xaouth_display_name=Chromium", + signed_text); +} + +TEST(OAuthRequestSignerTest, ParseAndSignPost2) { + GURL request_url("https://accounts.google.com/OAuthGetAccessToken" + "?oauth_timestamp=1234567890" + "&oauth_nonce=17171717171717171"); + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::ParseAndSign( + request_url, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::POST_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + "", // token secret + &signed_text)); + ASSERT_EQ(signed_text, + "oauth_consumer_key=anonymous" + "&oauth_nonce=17171717171717171" + "&oauth_signature=tPX2XqKQICWzopZ80CFGX%2F53DLo%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1234567890" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0"); +} + +TEST(OAuthRequestSignerTest, SignAuthHeader) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken"); + OAuthRequestSigner::Parameters parameters; + parameters["scope"] = "https://accounts.google.com/OAuthLogin"; + parameters["oauth_nonce"] = "2oiE_aHdk5qRTz0L9C8Lq0g"; + parameters["xaouth_display_name"] = "Chromium"; + parameters["oauth_timestamp"] = "1308152953"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignAuthHeader( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "johndoe", // oauth_consumer_key + "53cR3t", // consumer secret + "4/VGY0MsQadcmO8VnCv9gnhoEooq1v", // oauth_token + "c5e0531ff55dfbb4054e", // token secret + &signed_text)); + ASSERT_EQ("OAuth " + "oauth_consumer_key=\"johndoe\", " + "oauth_nonce=\"2oiE_aHdk5qRTz0L9C8Lq0g\", " + "oauth_signature=\"PFqDTaiyey1UObcvOyI4Ng2HXW0%3D\", " + "oauth_signature_method=\"HMAC-SHA1\", " + "oauth_timestamp=\"1308152953\", " + "oauth_token=\"4%2FVGY0MsQadcmO8VnCv9gnhoEooq1v\", " + "oauth_version=\"1.0\", " + "scope=\"https%3A%2F%2Faccounts.google.com%2FOAuthLogin\", " + "xaouth_display_name=\"Chromium\"", + signed_text); +} |