diff options
-rw-r--r-- | chrome/chrome_common.gypi | 3 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 | ||||
-rw-r--r-- | chrome/common/net/gaia/gaia_auth_consumer.h | 5 | ||||
-rw-r--r-- | chrome/common/net/gaia/gaia_auth_fetcher.cc | 227 | ||||
-rw-r--r-- | chrome/common/net/gaia/gaia_auth_fetcher.h | 61 | ||||
-rw-r--r-- | chrome/common/net/gaia/gaia_auth_fetcher_unittest.cc | 224 | ||||
-rw-r--r-- | chrome/common/net/gaia/gaia_urls.cc | 35 | ||||
-rw-r--r-- | chrome/common/net/gaia/gaia_urls.h | 10 | ||||
-rw-r--r-- | chrome/common/net/gaia/oauth2_access_token_consumer.h | 23 | ||||
-rw-r--r-- | chrome/common/net/gaia/oauth2_access_token_fetcher.cc | 185 | ||||
-rw-r--r-- | chrome/common/net/gaia/oauth2_access_token_fetcher.h | 94 | ||||
-rw-r--r-- | chrome/common/net/gaia/oauth2_access_token_fetcher_unittest.cc | 172 |
12 files changed, 1036 insertions, 4 deletions
diff --git a/chrome/chrome_common.gypi b/chrome/chrome_common.gypi index 6ecfc50..0111b2d 100644 --- a/chrome/chrome_common.gypi +++ b/chrome/chrome_common.gypi @@ -375,6 +375,9 @@ 'common/net/gaia/google_service_auth_error.h', 'common/net/gaia/oauth_request_signer.cc', 'common/net/gaia/oauth_request_signer.h', + 'common/net/gaia/oauth2_access_token_consumer.h', + 'common/net/gaia/oauth2_access_token_fetcher.cc', + 'common/net/gaia/oauth2_access_token_fetcher.h', 'common/net/x509_certificate_model.cc', 'common/net/x509_certificate_model_nss.cc', 'common/net/x509_certificate_model_openssl.cc', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 26fbe2f..a1a4179 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1966,6 +1966,7 @@ 'common/net/gaia/gaia_oauth_client_unittest.cc', 'common/net/gaia/google_service_auth_error_unittest.cc', 'common/net/gaia/oauth_request_signer_unittest.cc', + 'common/net/gaia/oauth2_access_token_fetcher_unittest.cc', 'common/random_unittest.cc', 'common/service_process_util_unittest.cc', 'common/string_ordinal_unittest.cc', diff --git a/chrome/common/net/gaia/gaia_auth_consumer.h b/chrome/common/net/gaia/gaia_auth_consumer.h index fdca0ab..915ba40 100644 --- a/chrome/common/net/gaia/gaia_auth_consumer.h +++ b/chrome/common/net/gaia/gaia_auth_consumer.h @@ -42,6 +42,11 @@ class GaiaAuthConsumer { virtual void OnIssueAuthTokenFailure(const std::string& service, const GoogleServiceAuthError& error) {} + virtual void OnOAuthLoginTokenSuccess(const std::string& refresh_token, + const std::string& access_token, + int expires_in_secs) {} + virtual void OnOAuthLoginTokenFailure(const GoogleServiceAuthError& error) {} + virtual void OnGetUserInfoSuccess(const std::string& key, const std::string& value) {} virtual void OnGetUserInfoKeyNotFound(const std::string& key) {} diff --git a/chrome/common/net/gaia/gaia_auth_fetcher.cc b/chrome/common/net/gaia/gaia_auth_fetcher.cc index 0a861e6..f3f86d3 100644 --- a/chrome/common/net/gaia/gaia_auth_fetcher.cc +++ b/chrome/common/net/gaia/gaia_auth_fetcher.cc @@ -4,13 +4,16 @@ #include "chrome/common/net/gaia/gaia_auth_fetcher.h" +#include <algorithm> #include <string> #include <utility> #include <vector> +#include "base/json/json_reader.h" #include "base/string_split.h" #include "base/string_util.h" #include "base/stringprintf.h" +#include "base/values.h" #include "chrome/common/net/gaia/gaia_auth_consumer.h" #include "chrome/common/net/gaia/gaia_constants.h" #include "chrome/common/net/gaia/gaia_urls.h" @@ -22,6 +25,13 @@ #include "net/url_request/url_request_context_getter.h" #include "net/url_request/url_request_status.h" +namespace { +static bool CookiePartsContains(const std::vector<std::string>& parts, + const char* part) { + return std::find(parts.begin(), parts.end(), part) != parts.end(); +} +} // namespace + // TODO(chron): Add sourceless version of this formatter. // static const char GaiaAuthFetcher::kClientLoginFormat[] = @@ -48,6 +58,16 @@ const char GaiaAuthFetcher::kIssueAuthTokenFormat[] = "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 @@ -100,6 +120,27 @@ const char GaiaAuthFetcher::kAccountTypeGoogle[] = // static const char GaiaAuthFetcher::kSecondFactor[] = "Info=InvalidSecondFactor"; +// static +const char GaiaAuthFetcher::kAuthHeaderFormat[] = + "Authorization: GoogleLogin auth=%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; +// static +const char GaiaAuthFetcher::kOAuth2RefreshTokenKey[] = "refresh_token"; +// static +const char GaiaAuthFetcher::kOAuth2AccessTokenKey[] = "access_token"; +// static +const char GaiaAuthFetcher::kOAuth2ExpiresInKey[] = "expires_in"; + GaiaAuthFetcher::GaiaAuthFetcher(GaiaAuthConsumer* consumer, const std::string& source, net::URLRequestContextGetter* getter) @@ -108,6 +149,9 @@ GaiaAuthFetcher::GaiaAuthFetcher(GaiaAuthConsumer* consumer, source_(source), client_login_gurl_(GaiaUrls::GetInstance()->client_login_url()), issue_auth_token_gurl_(GaiaUrls::GetInstance()->issue_auth_token_url()), + client_login_to_oauth2_gurl_( + GaiaUrls::GetInstance()->client_login_to_oauth2_url()), + oauth2_token_gurl_(GaiaUrls::GetInstance()->oauth2_token_url()), get_user_info_gurl_(GaiaUrls::GetInstance()->get_user_info_url()), token_auth_gurl_(GaiaUrls::GetInstance()->token_auth_url()), merge_session_gurl_(GaiaUrls::GetInstance()->merge_session_url()), @@ -128,8 +172,9 @@ void GaiaAuthFetcher::CancelRequest() { content::URLFetcher* GaiaAuthFetcher::CreateGaiaFetcher( net::URLRequestContextGetter* getter, const std::string& body, + const std::string& headers, const GURL& gaia_gurl, - bool send_cookies, + bool use_cookies, content::URLFetcherDelegate* delegate) { content::URLFetcher* to_return = content::URLFetcher::Create( 0, gaia_gurl, content::URLFetcher::POST, delegate); @@ -141,8 +186,12 @@ content::URLFetcher* GaiaAuthFetcher::CreateGaiaFetcher( // maintain a separation between the user's browsing and Chrome's internal // services. Where such mixing is desired (MergeSession), it will be done // explicitly. - if (!send_cookies) - to_return->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES); + if (!use_cookies) { + to_return->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + } + if (!headers.empty()) + to_return->SetExtraRequestHeaders(headers); return to_return; } @@ -209,6 +258,34 @@ std::string GaiaAuthFetcher::MakeIssueAuthTokenBody( } // 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()); @@ -243,6 +320,12 @@ std::string GaiaAuthFetcher::MakeMergeSessionBody( 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, @@ -293,6 +376,72 @@ void GaiaAuthFetcher::ParseClientLoginFailure(const std::string& data, } } +// 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::ParseOAuth2TokenPairResponse(const std::string& data, + std::string* refresh_token, + std::string* access_token, + int* expires_in_secs) { + DCHECK(refresh_token); + DCHECK(access_token); + base::JSONReader reader; + scoped_ptr<base::Value> value(reader.Read(data, false)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + return false; + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + std::string rt; + std::string at; + int exp; + + if (!dict->GetStringWithoutPathExpansion(kOAuth2RefreshTokenKey, &rt) || + !dict->GetStringWithoutPathExpansion(kOAuth2AccessTokenKey, &at) || + !dict->GetIntegerWithoutPathExpansion(kOAuth2ExpiresInKey, &exp)) { + return false; + } + + refresh_token->assign(rt); + access_token->assign(at); + *expires_in_secs = exp; + return true; +} + +// 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; +} + void GaiaAuthFetcher::StartClientLogin( const std::string& username, const std::string& password, @@ -317,6 +466,7 @@ void GaiaAuthFetcher::StartClientLogin( allow_hosted_accounts); fetcher_.reset(CreateGaiaFetcher(getter_, request_body_, + "", client_login_gurl_, false, this)); @@ -334,6 +484,7 @@ void GaiaAuthFetcher::StartIssueAuthToken(const std::string& sid, request_body_ = MakeIssueAuthTokenBody(sid, lsid, service); fetcher_.reset(CreateGaiaFetcher(getter_, request_body_, + "", issue_auth_token_gurl_, false, this)); @@ -341,6 +492,22 @@ void GaiaAuthFetcher::StartIssueAuthToken(const std::string& sid, fetcher_->Start(); } +void GaiaAuthFetcher::StartOAuthLoginTokenFetch( + const std::string& auth_token) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting OAuth login token fetch"; + request_body_ = MakeGetAuthCodeBody(); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + MakeGetAuthCodeHeader(auth_token), + client_login_to_oauth2_gurl_, + false, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + void GaiaAuthFetcher::StartGetUserInfo(const std::string& lsid, const std::string& info_key) { DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; @@ -349,6 +516,7 @@ void GaiaAuthFetcher::StartGetUserInfo(const std::string& lsid, request_body_ = MakeGetUserInfoBody(lsid); fetcher_.reset(CreateGaiaFetcher(getter_, request_body_, + "", get_user_info_gurl_, false, this)); @@ -369,6 +537,7 @@ void GaiaAuthFetcher::StartTokenAuth(const std::string& auth_token) { request_body_ = MakeTokenAuthBody(auth_token, continue_url, source_); fetcher_.reset(CreateGaiaFetcher(getter_, request_body_, + "", token_auth_gurl_, false, this)); @@ -393,6 +562,7 @@ void GaiaAuthFetcher::StartMergeSession(const std::string& auth_token) { request_body_ = MakeMergeSessionBody(auth_token, continue_url, source_); fetcher_.reset(CreateGaiaFetcher(getter_, request_body_, + "", merge_session_gurl_, true, this)); @@ -536,6 +706,52 @@ void GaiaAuthFetcher::OnIssueAuthTokenFetched( } } +void GaiaAuthFetcher::OnClientLoginToOAuth2Fetched( + const std::string& data, + const net::ResponseCookies& cookies, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == RC_REQUEST_OK) { + std::string auth_code; + ParseClientLoginToOAuth2Response(cookies, &auth_code); + StartOAuth2TokenPairFetch(auth_code); + } else { + consumer_->OnOAuthLoginTokenFailure(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_, + false, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::OnOAuth2TokenPairFetched( + const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == RC_REQUEST_OK) { + std::string refresh_token; + std::string access_token; + int expires_in_secs; + ParseOAuth2TokenPairResponse( + data, &refresh_token, &access_token, &expires_in_secs); + consumer_->OnOAuthLoginTokenSuccess( + refresh_token, access_token, expires_in_secs); + } else { + consumer_->OnOAuthLoginTokenFailure(GenerateAuthError(data, status)); + } +} + void GaiaAuthFetcher::OnGetUserInfoFetched( const std::string& data, const net::URLRequestStatus& status, @@ -591,6 +807,11 @@ void GaiaAuthFetcher::OnURLFetchComplete(const content::URLFetcher* source) { 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 == token_auth_gurl_) { diff --git a/chrome/common/net/gaia/gaia_auth_fetcher.h b/chrome/common/net/gaia/gaia_auth_fetcher.h index 646ed95..fa4d3cc 100644 --- a/chrome/common/net/gaia/gaia_auth_fetcher.h +++ b/chrome/common/net/gaia/gaia_auth_fetcher.h @@ -7,6 +7,7 @@ #pragma once #include <string> +#include <vector> #include "base/gtest_prod_util.h" #include "base/memory/scoped_ptr.h" @@ -29,6 +30,7 @@ class GaiaAuthFetcherTest; namespace net { class URLRequestContextGetter; class URLRequestStatus; +typedef std::vector<std::string> ResponseCookies; } class GaiaAuthFetcher : public content::URLFetcherDelegate { @@ -66,6 +68,12 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { const std::string& lsid, const char* const service); + // Start fetching OAuth login scoped token from the given ClientLogin token + // for "lso" service. + // Either OnOAuthLoginTokenSuccess or OnOAuthLoginTokenFailure method will be + // called on the consumer with results. + void StartOAuthLoginTokenFetch(const std::string& auth_token); + // Start a request to get a particular key from user info. // GaiaAuthConsumer will be called back on the same thread when // results come back. @@ -109,6 +117,10 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { 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 TokenAuth. @@ -132,6 +144,16 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { static const char kCaptchaUrlParam[]; static const char kCaptchaTokenParam[]; + // Constants for request/response for OAuth2 requests. + static const char kAuthHeaderFormat[]; + static const char kClientLoginToOAuth2CookiePartSecure[]; + static const char kClientLoginToOAuth2CookiePartHttpOnly[]; + static const char kClientLoginToOAuth2CookiePartCodePrefix[]; + static const int kClientLoginToOAuth2CookiePartCodePrefixLength; + static const char kOAuth2RefreshTokenKey[]; + static const char kOAuth2AccessTokenKey[]; + static const char kOAuth2ExpiresInKey[]; + // Process the results of a ClientLogin fetch. void OnClientLoginFetched(const std::string& data, const net::URLRequestStatus& status, @@ -141,6 +163,15 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { 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); @@ -165,6 +196,20 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { std::string* captcha_url, std::string* captcha_token); + // Parse ClientLogin to OAuth2 response. + static bool ParseClientLoginToOAuth2Response( + const net::ResponseCookies& cookies, + std::string* auth_code); + + // Parse OAuth2 token pairresponse. + static bool ParseOAuth2TokenPairResponse(const std::string& data, + std::string* refresh_token, + std::string* access_token, + int* expires_in_secs); + + static bool ParseClientLoginToOAuth2Cookie(const std::string& cookie, + std::string* auth_code); + // Is this a special case Gaia error for TwoFactor auth? static bool IsSecondFactorSuccess(const std::string& alleged_error); @@ -182,6 +227,10 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { 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); @@ -196,12 +245,17 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { const std::string& continue_url, const std::string& source); + static std::string MakeGetAuthCodeHeader(const std::string& auth_token); + + void StartOAuth2TokenPairFetch(const std::string& auth_code); + // Create a fetcher useable for making any Gaia request. static content::URLFetcher* CreateGaiaFetcher( net::URLRequestContextGetter* getter, const std::string& body, + const std::string& headers, const GURL& gaia_gurl, - bool send_cookies, + bool use_cookies, content::URLFetcherDelegate* delegate); // From a URLFetcher result, generate an appropriate error. @@ -217,6 +271,8 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { std::string source_; const GURL client_login_gurl_; const GURL issue_auth_token_gurl_; + const GURL client_login_to_oauth2_gurl_; + const GURL oauth2_token_gurl_; const GURL get_user_info_gurl_; const GURL token_auth_gurl_; const GURL merge_session_gurl_; @@ -238,6 +294,9 @@ class GaiaAuthFetcher : public content::URLFetcherDelegate { 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); DISALLOW_COPY_AND_ASSIGN(GaiaAuthFetcher); }; diff --git a/chrome/common/net/gaia/gaia_auth_fetcher_unittest.cc b/chrome/common/net/gaia/gaia_auth_fetcher_unittest.cc index f1f209f..8a17a83 100644 --- a/chrome/common/net/gaia/gaia_auth_fetcher_unittest.cc +++ b/chrome/common/net/gaia/gaia_auth_fetcher_unittest.cc @@ -28,6 +28,42 @@ using ::testing::_; +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 kGetTokenPairResponseNoRefreshToken[] = + "{" + " \"access_token\": \"at1\"," + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + "}"; +static const char kGetTokenPairResponseNoAccessToken[] = + "{" + " \"refresh_token\": \"rt1\"," + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + "}"; +static const char kGetTokenPairResponseNoExpiresIn[] = + "{" + " \"refresh_token\": \"rt1\"," + " \"access_token\": \"at1\"," + " \"token_type\": \"Bearer\"" + "}"; +} // namespace + MockFetcher::MockFetcher(bool success, const GURL& url, const std::string& results, @@ -76,6 +112,9 @@ class GaiaAuthFetcherTest : public testing::Test { : 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()) {} @@ -120,6 +159,8 @@ class GaiaAuthFetcherTest : public testing::Test { 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_; TestingProfile profile_; @@ -135,12 +176,18 @@ class MockGaiaConsumer : public GaiaAuthConsumer { MOCK_METHOD1(OnClientLoginSuccess, void(const ClientLoginResult& result)); MOCK_METHOD2(OnIssueAuthTokenSuccess, void(const std::string& service, const std::string& token)); + MOCK_METHOD3(OnOAuthLoginTokenSuccess, + void(const std::string& refresh_token, + const std::string& access_token, + int expires_in_secs)); MOCK_METHOD1(OnTokenAuthSuccess, void(const std::string& data)); MOCK_METHOD1(OnMergeSessionSuccess, 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(OnOAuthLoginTokenFailure, + void(const GoogleServiceAuthError& error)); MOCK_METHOD1(OnTokenAuthFailure, void(const GoogleServiceAuthError& error)); MOCK_METHOD1(OnMergeSessionFailure, void( const GoogleServiceAuthError& error)); @@ -523,6 +570,91 @@ TEST_F(GaiaAuthFetcherTest, FullTokenFailure) { EXPECT_FALSE(auth.HasPendingFetch()); } +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnOAuthLoginTokenSuccess("rt1", "at1", 3600)) + .Times(1); + + TestingProfile profile; + + TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartOAuthLoginTokenFetch("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), + RC_REQUEST_OK, cookies, "", + content::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher1); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher2( + oauth2_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + RC_REQUEST_OK, cookies_, kGetTokenPairValidResponse, + content::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher2); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenClientLoginToOAuth2Failure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnOAuthLoginTokenFailure(_)) + .Times(1); + + TestingProfile profile; + + TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartOAuthLoginTokenFetch("lso_token"); + + net::ResponseCookies cookies; + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + client_login_to_oauth2_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + RC_FORBIDDEN, cookies, "", + content::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenOAuth2TokenPairFailure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnOAuthLoginTokenFailure(_)) + .Times(1); + + TestingProfile profile; + + TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), + profile_.GetRequestContext()); + auth.StartOAuthLoginTokenFetch("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), + RC_REQUEST_OK, cookies, "", + content::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher1); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher2( + oauth2_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + RC_FORBIDDEN, cookies_, "", + content::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher2); + EXPECT_FALSE(auth.HasPendingFetch()); +} + TEST_F(GaiaAuthFetcherTest, TokenAuthSuccess) { MockGaiaConsumer consumer; EXPECT_CALL(consumer, OnTokenAuthSuccess("<html></html>")) @@ -639,3 +771,95 @@ TEST_F(GaiaAuthFetcherTest, MergeSessionSuccessRedirect) { auth.OnURLFetchComplete(test_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, ParseOAuth2TokenPairResponse) { + { // No data. + std::string rt; + std::string at; + int exp = -1; + EXPECT_FALSE(GaiaAuthFetcher::ParseOAuth2TokenPairResponse( + "", &rt, &at, &exp)); + EXPECT_EQ("", rt); + EXPECT_EQ("", at); + EXPECT_EQ(-1, exp); + } + { // No refresh token. + std::string rt; + std::string at; + int exp = -1; + EXPECT_FALSE(GaiaAuthFetcher::ParseOAuth2TokenPairResponse( + kGetTokenPairResponseNoRefreshToken, &rt, &at, &exp)); + EXPECT_EQ("", rt); + EXPECT_EQ("", at); + EXPECT_EQ(-1, exp); + } + { // No access token. + std::string rt; + std::string at; + int exp = -1; + EXPECT_FALSE(GaiaAuthFetcher::ParseOAuth2TokenPairResponse( + kGetTokenPairResponseNoAccessToken, &rt, &at, &exp)); + EXPECT_EQ("", rt); + EXPECT_EQ("", at); + EXPECT_EQ(-1, exp); + } + { // No expiration. + std::string rt; + std::string at; + int exp = -1; + EXPECT_FALSE(GaiaAuthFetcher::ParseOAuth2TokenPairResponse( + kGetTokenPairResponseNoExpiresIn, &rt, &at, &exp)); + EXPECT_EQ("", rt); + EXPECT_EQ("", at); + EXPECT_EQ(-1, exp); + } + { // Valid response. + std::string rt; + std::string at; + int exp = -1; + EXPECT_TRUE(GaiaAuthFetcher::ParseOAuth2TokenPairResponse( + kGetTokenPairValidResponse, &rt, &at, &exp)); + EXPECT_EQ("rt1", rt); + EXPECT_EQ("at1", at); + EXPECT_EQ(3600, exp); + } +} diff --git a/chrome/common/net/gaia/gaia_urls.cc b/chrome/common/net/gaia/gaia_urls.cc index 0d1f424..49a33c0 100644 --- a/chrome/common/net/gaia/gaia_urls.cc +++ b/chrome/common/net/gaia/gaia_urls.cc @@ -24,6 +24,20 @@ const char kOAuthWrapBridgeUrlSuffix[] = "/accounts/OAuthWrapBridge"; const char kOAuth1LoginUrlSuffix[] = "/accounts/OAuthLogin"; const char kOAuthRevokeTokenUrlSuffix[] = "/accounts/AuthSubRevokeToken"; +// OAuth2 client id for Google Chrome which is registered as an +// installed application. +static const char kOAuth2ChromeClientId[] = + "77185425430.apps.googleusercontent.com"; +// For an installed application, client secret is not really a secret since +// it is expected to be embeeded in the application. +// See documentation at +// http://code.google.com/apis/accounts/docs/OAuth2InstalledApp.html +static const char kOAuth2ChromeClientSecret[] = + "OTJgUOQcT7lO7GsGZq2G4IlT"; +const char kClientLoginToOAuth2Url[] = + "https://accounts.google.com/o/oauth2/programmatic_auth"; +const char kOAuth2TokenUrl[] = + "https://accounts.google.com/o/oauth2/token"; } // namespacce GaiaUrls* GaiaUrls::GetInstance() { @@ -61,6 +75,11 @@ GaiaUrls::GaiaUrls() { oauth_user_info_url_ = "https://www.googleapis.com/oauth2/v1/userinfo"; oauth_wrap_bridge_user_info_scope_ = "https://www.googleapis.com/auth/userinfo.email"; + + oauth2_chrome_client_id_ = kOAuth2ChromeClientId; + oauth2_chrome_client_secret_ = kOAuth2ChromeClientSecret; + client_login_to_oauth2_url_ = kClientLoginToOAuth2Url; + oauth2_token_url_ = kOAuth2TokenUrl; } GaiaUrls::~GaiaUrls() { @@ -125,3 +144,19 @@ const std::string& GaiaUrls::oauth1_login_scope() { const std::string& GaiaUrls::oauth_wrap_bridge_user_info_scope() { return oauth_wrap_bridge_user_info_scope_; } + +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_; +} diff --git a/chrome/common/net/gaia/gaia_urls.h b/chrome/common/net/gaia/gaia_urls.h index 50121a5..c8c1b46 100644 --- a/chrome/common/net/gaia/gaia_urls.h +++ b/chrome/common/net/gaia/gaia_urls.h @@ -34,6 +34,11 @@ class GaiaUrls { const std::string& oauth1_login_scope(); const std::string& oauth_wrap_bridge_user_info_scope(); + 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(); + private: GaiaUrls(); ~GaiaUrls(); @@ -58,6 +63,11 @@ class GaiaUrls { std::string oauth1_login_scope_; std::string oauth_wrap_bridge_user_info_scope_; + std::string oauth2_chrome_client_id_; + std::string oauth2_chrome_client_secret_; + std::string client_login_to_oauth2_url_; + std::string oauth2_token_url_; + DISALLOW_COPY_AND_ASSIGN(GaiaUrls); }; diff --git a/chrome/common/net/gaia/oauth2_access_token_consumer.h b/chrome/common/net/gaia/oauth2_access_token_consumer.h new file mode 100644 index 0000000..cf7eb34 --- /dev/null +++ b/chrome/common/net/gaia/oauth2_access_token_consumer.h @@ -0,0 +1,23 @@ +// 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 CHROME_COMMON_NET_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ +#define CHROME_COMMON_NET_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ +#pragma once + +#include <string> + +class GoogleServiceAuthError; + +// An interface that defines the callbacks for consumers to which +// OAuth2AccessTokenFetcher can return results. +class OAuth2AccessTokenConsumer { + public: + virtual ~OAuth2AccessTokenConsumer() {} + + virtual void OnGetTokenSuccess(const std::string& access_token) {} + virtual void OnGetTokenFailure(const GoogleServiceAuthError& error) {} +}; + +#endif // CHROME_COMMON_NET_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ diff --git a/chrome/common/net/gaia/oauth2_access_token_fetcher.cc b/chrome/common/net/gaia/oauth2_access_token_fetcher.cc new file mode 100644 index 0000000..7f4e256b07 --- /dev/null +++ b/chrome/common/net/gaia/oauth2_access_token_fetcher.cc @@ -0,0 +1,185 @@ +// 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 "chrome/common/net/gaia/oauth2_access_token_fetcher.h" + +#include <algorithm> +#include <string> + +#include "base/json/json_reader.h" +#include "base/stringprintf.h" +#include "base/values.h" +#include "chrome/common/net/gaia/gaia_urls.h" +#include "chrome/common/net/gaia/google_service_auth_error.h" +#include "chrome/common/net/http_return.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using content::URLFetcher; +using content::URLFetcherDelegate; +using net::ResponseCookies; +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 kAccessTokenKey[] = "access_token"; + +static bool GetStringFromDictionary(const DictionaryValue* dict, + const std::string& key, + std::string* value) { + Value* json_value; + if (!dict->Get(key, &json_value)) + return false; + if (json_value->GetType() != base::Value::TYPE_STRING) + return false; + + StringValue* json_str_value = static_cast<StringValue*>(json_value); + json_str_value->GetAsString(value); + return true; +} + +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 = 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, + const std::string& source) + : consumer_(consumer), + getter_(getter), + source_(source), + state_(INITIAL) { } + +OAuth2AccessTokenFetcher::~OAuth2AccessTokenFetcher() { } + +void OAuth2AccessTokenFetcher::CancelRequest() { + fetcher_.reset(); +} + +void OAuth2AccessTokenFetcher::Start(const std::string& refresh_token) { + refresh_token_ = refresh_token; + StartGetAccessToken(); +} + +void OAuth2AccessTokenFetcher::StartGetAccessToken() { + CHECK_EQ(INITIAL, state_); + state_ = GET_ACCESS_TOKEN_STARTED; + fetcher_.reset(CreateFetcher( + getter_, + MakeGetAccessTokenUrl(), + MakeGetAccessTokenBody(refresh_token_), + this)); + fetcher_->Start(); // OnURLFetchComplete will be called. +} + +void OAuth2AccessTokenFetcher::EndGetAccessToken(const 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() != RC_REQUEST_OK) { + OnGetTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + return; + } + + // The request was successfully fetched and it returned OK. + // Parse out the access token. + std::string access_token; + ParseGetAccessTokenResponse(source, &access_token); + OnGetTokenSuccess(access_token); +} + +void OAuth2AccessTokenFetcher::OnGetTokenSuccess( + const std::string& access_token) { + consumer_->OnGetTokenSuccess(access_token); +} + +void OAuth2AccessTokenFetcher::OnGetTokenFailure(GoogleServiceAuthError error) { + state_ = ERROR_STATE; + consumer_->OnGetTokenFailure(error); +} + +void OAuth2AccessTokenFetcher::OnURLFetchComplete(const 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& refresh_token) { + return StringPrintf( + kGetAccessTokenBodyFormat, + net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth2_chrome_client_id(), true).c_str(), + net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), + true).c_str(), + net::EscapeUrlEncodedData(refresh_token, true).c_str()); +} + +// static +bool OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + const URLFetcher* source, + std::string* access_token) { + CHECK(source); + CHECK(access_token); + std::string data; + source->GetResponseAsString(&data); + base::JSONReader reader; + scoped_ptr<base::Value> value(reader.Read(data, false)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + return false; + + DictionaryValue* dict = static_cast<DictionaryValue*>(value.get()); + return GetStringFromDictionary(dict, kAccessTokenKey, access_token); +} diff --git a/chrome/common/net/gaia/oauth2_access_token_fetcher.h b/chrome/common/net/gaia/oauth2_access_token_fetcher.h new file mode 100644 index 0000000..5ba4af7 --- /dev/null +++ b/chrome/common/net/gaia/oauth2_access_token_fetcher.h @@ -0,0 +1,94 @@ +// 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 CHROME_COMMON_NET_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ +#define CHROME_COMMON_NET_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ +#pragma once + +#include <string> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "chrome/common/net/gaia/oauth2_access_token_consumer.h" +#include "content/public/common/url_fetcher.h" +#include "content/public/common/url_fetcher_delegate.h" +#include "googleurl/src/gurl.h" + +class OAuth2AccessTokenFetcherTest; + +namespace net { +class URLRequestContextGetter; +class URLRequestStatus; +} + +// Abstracts the details to get OAuth2 access token token from +// OAuth2 refresh 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 OAuth2AccessTokenFetcher : public content::URLFetcherDelegate { + public: + OAuth2AccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, + net::URLRequestContextGetter* getter, + const std::string& source); + virtual ~OAuth2AccessTokenFetcher(); + + void Start(const std::string& refresh_token); + + void CancelRequest(); + + // Implementation of content::URLFetcherDelegate + virtual void OnURLFetchComplete(const content::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 content::URLFetcher* source); + + // Helper mehtods for reporting back results. + void OnGetTokenSuccess(const std::string& access_token); + void OnGetTokenFailure(GoogleServiceAuthError error); + + // Other helpers. + static GURL MakeGetAccessTokenUrl(); + static std::string MakeGetAccessTokenBody(const std::string& refresh_token); + static bool ParseGetAccessTokenResponse(const content::URLFetcher* source, + std::string* access_token); + + // State that is set during construction. + OAuth2AccessTokenConsumer* const consumer_; + net::URLRequestContextGetter* const getter_; + std::string source_; + State state_; + + // While a fetch is in progress. + scoped_ptr<content::URLFetcher> fetcher_; + std::string refresh_token_; + + friend class OAuth2AccessTokenFetcherTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherTest, + ParseGetAccessTokenResponse); + + DISALLOW_COPY_AND_ASSIGN(OAuth2AccessTokenFetcher); +}; + +#endif // CHROME_COMMON_NET_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ diff --git a/chrome/common/net/gaia/oauth2_access_token_fetcher_unittest.cc b/chrome/common/net/gaia/oauth2_access_token_fetcher_unittest.cc new file mode 100644 index 0000000..cbf3be5 --- /dev/null +++ b/chrome/common/net/gaia/oauth2_access_token_fetcher_unittest.cc @@ -0,0 +1,172 @@ +// 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. +// +// A complete set of unit tests for OAuth2AccessTokenFetcher. + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "chrome/common/net/gaia/gaia_urls.h" +#include "chrome/common/net/gaia/google_service_auth_error.h" +#include "chrome/common/net/http_return.h" +#include "chrome/common/net/gaia/oauth2_access_token_consumer.h" +#include "chrome/common/net/gaia/oauth2_access_token_fetcher.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/common/url_fetcher.h" +#include "content/public/common/url_fetcher_delegate.h" +#include "content/public/common/url_fetcher_factory.h" +#include "content/test/test_browser_thread.h" +#include "content/test/test_url_fetcher_factory.h" +#include "googleurl/src/gurl.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 content::URLFetcher; +using content::URLFetcherDelegate; +using content::URLFetcherFactory; +using net::ResponseCookies; +using net::URLRequestStatus; +using testing::_; +using testing::Return; + +namespace { +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_METHOD1(OnGetTokenSuccess, void(const std::string& access_token)); + MOCK_METHOD1(OnGetTokenFailure, + void(const GoogleServiceAuthError& error)); +}; + +class OAuth2AccessTokenFetcherTest : public testing::Test { + public: + OAuth2AccessTokenFetcherTest() + : ui_thread_(BrowserThread::UI, &message_loop_), + fetcher_(&consumer_, profile_.GetRequestContext(), "test") { + } + + 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_; +}; + +TEST_F(OAuth2AccessTokenFetcherTest, GetAccessTokenRequestFailure) { + TestURLFetcher* url_fetcher = SetupGetAccessToken(false, 0, ""); + EXPECT_CALL(consumer_, OnGetTokenFailure(_)).Times(1); + fetcher_.Start("refresh_token"); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, GetAccessTokenResponseCodeFailure) { + TestURLFetcher* url_fetcher = SetupGetAccessToken(true, RC_FORBIDDEN, ""); + EXPECT_CALL(consumer_, OnGetTokenFailure(_)).Times(1); + fetcher_.Start("refresh_token"); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, Success) { + TestURLFetcher* url_fetcher = SetupGetAccessToken( + true, RC_REQUEST_OK, kValidTokenResponse); + EXPECT_CALL(consumer_, OnGetTokenSuccess("at1")).Times(1); + fetcher_.Start("refresh_token"); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, ParseGetAccessTokenResponse) { + { // No body. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + + std::string at; + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &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(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &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(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &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(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at)); + EXPECT_EQ("at1", at); + } +} |