summaryrefslogtreecommitdiffstats
path: root/google_apis/gaia
diff options
context:
space:
mode:
authorjoi@chromium.org <joi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-09-07 04:26:37 +0000
committerjoi@chromium.org <joi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-09-07 04:26:37 +0000
commit6386cf58a85361fa20bba6ecfc23502e922b9a90 (patch)
tree06f9a9aa7e6bb8db149b53bc7489e69fa87c07fb /google_apis/gaia
parentf2857ecf6c8da488feeca9b05cf2c046ea43e02f (diff)
downloadchromium_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')
-rw-r--r--google_apis/gaia/DEPS6
-rw-r--r--google_apis/gaia/OWNERS3
-rw-r--r--google_apis/gaia/gaia_auth_consumer.cc48
-rw-r--r--google_apis/gaia/gaia_auth_consumer.h85
-rw-r--r--google_apis/gaia/gaia_auth_fetcher.cc1106
-rw-r--r--google_apis/gaia/gaia_auth_fetcher.h406
-rw-r--r--google_apis/gaia/gaia_auth_fetcher_unittest.cc1021
-rw-r--r--google_apis/gaia/gaia_auth_util.cc61
-rw-r--r--google_apis/gaia/gaia_auth_util.h31
-rw-r--r--google_apis/gaia/gaia_auth_util_unittest.cc87
-rw-r--r--google_apis/gaia/gaia_authenticator.cc400
-rw-r--r--google_apis/gaia/gaia_authenticator.h273
-rw-r--r--google_apis/gaia/gaia_authenticator_unittest.cc49
-rw-r--r--google_apis/gaia/gaia_constants.cc95
-rw-r--r--google_apis/gaia/gaia_constants.h53
-rw-r--r--google_apis/gaia/gaia_oauth_client.cc208
-rw-r--r--google_apis/gaia/gaia_oauth_client.h74
-rw-r--r--google_apis/gaia/gaia_oauth_client_unittest.cc243
-rw-r--r--google_apis/gaia/gaia_switches.cc18
-rw-r--r--google_apis/gaia/gaia_switches.h44
-rw-r--r--google_apis/gaia/gaia_urls.cc242
-rw-r--r--google_apis/gaia/gaia_urls.h83
-rw-r--r--google_apis/gaia/google_service_auth_error.cc285
-rw-r--r--google_apis/gaia/google_service_auth_error.h209
-rw-r--r--google_apis/gaia/google_service_auth_error_unittest.cc124
-rw-r--r--google_apis/gaia/mock_url_fetcher_factory.h70
-rw-r--r--google_apis/gaia/oauth2_access_token_consumer.h31
-rw-r--r--google_apis/gaia/oauth2_access_token_fetcher.cc218
-rw-r--r--google_apis/gaia/oauth2_access_token_fetcher.h118
-rw-r--r--google_apis/gaia/oauth2_access_token_fetcher_unittest.cc235
-rw-r--r--google_apis/gaia/oauth2_api_call_flow.cc167
-rw-r--r--google_apis/gaia/oauth2_api_call_flow.h128
-rw-r--r--google_apis/gaia/oauth2_api_call_flow_unittest.cc301
-rw-r--r--google_apis/gaia/oauth2_mint_token_consumer.h22
-rw-r--r--google_apis/gaia/oauth2_mint_token_fetcher.cc190
-rw-r--r--google_apis/gaia/oauth2_mint_token_fetcher.h104
-rw-r--r--google_apis/gaia/oauth2_mint_token_fetcher_unittest.cc179
-rw-r--r--google_apis/gaia/oauth2_mint_token_flow.cc261
-rw-r--r--google_apis/gaia/oauth2_mint_token_flow.h153
-rw-r--r--google_apis/gaia/oauth2_mint_token_flow_unittest.cc358
-rw-r--r--google_apis/gaia/oauth2_revocation_consumer.h22
-rw-r--r--google_apis/gaia/oauth2_revocation_fetcher.cc165
-rw-r--r--google_apis/gaia/oauth2_revocation_fetcher.h94
-rw-r--r--google_apis/gaia/oauth2_revocation_fetcher_unittest.cc122
-rw-r--r--google_apis/gaia/oauth_request_signer.cc458
-rw-r--r--google_apis/gaia/oauth_request_signer.h100
-rw-r--r--google_apis/gaia/oauth_request_signer_unittest.cc323
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, &parameters))
+ 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, &parameters);
+ 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, &parameters);
+ 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);
+}