diff options
-rw-r--r-- | chrome/chrome_common.gypi | 2 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 | ||||
-rw-r--r-- | chrome/common/net/gaia/oauth2_api_call_flow.cc | 152 | ||||
-rw-r--r-- | chrome/common/net/gaia/oauth2_api_call_flow.h | 126 | ||||
-rw-r--r-- | chrome/common/net/gaia/oauth2_api_call_flow_unittest.cc | 263 |
5 files changed, 544 insertions, 0 deletions
diff --git a/chrome/chrome_common.gypi b/chrome/chrome_common.gypi index c99d408..f9b6663 100644 --- a/chrome/chrome_common.gypi +++ b/chrome/chrome_common.gypi @@ -409,6 +409,8 @@ '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/gaia/oauth2_api_call_flow.cc', + 'common/net/gaia/oauth2_api_call_flow.h', 'common/net/gaia/oauth2_mint_token_consumer.h', 'common/net/gaia/oauth2_mint_token_fetcher.cc', 'common/net/gaia/oauth2_mint_token_fetcher.h', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 997ae3f..e180a03 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -2078,6 +2078,7 @@ '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/net/gaia/oauth2_api_call_flow_unittest.cc', 'common/net/gaia/oauth2_mint_token_fetcher_unittest.cc', 'common/net/gaia/oauth2_mint_token_flow_unittest.cc', 'common/net/gaia/oauth2_revocation_fetcher_unittest.cc', diff --git a/chrome/common/net/gaia/oauth2_api_call_flow.cc b/chrome/common/net/gaia/oauth2_api_call_flow.cc new file mode 100644 index 0000000..9f6330d --- /dev/null +++ b/chrome/common/net/gaia/oauth2_api_call_flow.cc @@ -0,0 +1,152 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/common/net/gaia/oauth2_api_call_flow.h" + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "chrome/common/net/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_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; + +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(); + return; + } else { + state_ = API_CALL_STARTED; + + url_fetcher_.reset(CreateURLFetcher()); + url_fetcher_->Start(); // OnURLFetchComplete will be called. + } +} + +void OAuth2ApiCallFlow::EndApiCall(const URLFetcher* source) { + CHECK_EQ(API_CALL_STARTED, state_); + state_ = API_CALL_DONE; + + URLRequestStatus status = source->GetStatus(); + if (!status.is_success()) { + 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_) + ProcessApiCallFailure(source); + else + BeginMintAccessToken(); + + return; + } + + if (source->GetResponseCode() != net::HTTP_OK) { + 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 URLFetcher* source) { + CHECK(source); + CHECK_EQ(API_CALL_STARTED, state_); + EndApiCall(source); +} + +void OAuth2ApiCallFlow::OnGetTokenSuccess(const std::string& access_token) { + 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 = 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); + + if (!empty_body) + result->SetUploadData("application/x-www-form-urlencoded", body); + + return result; +} diff --git a/chrome/common/net/gaia/oauth2_api_call_flow.h b/chrome/common/net/gaia/oauth2_api_call_flow.h new file mode 100644 index 0000000..639b45f --- /dev/null +++ b/chrome/common/net/gaia/oauth2_api_call_flow.h @@ -0,0 +1,126 @@ +// 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 CHROME_COMMON_NET_GAIA_OAUTH2_API_CALL_FLOW_H_ +#define CHROME_COMMON_NET_GAIA_OAUTH2_API_CALL_FLOW_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "chrome/common/net/gaia/oauth2_access_token_consumer.h" +#include "chrome/common/net/gaia/oauth2_access_token_fetcher.h" +#include "chrome/common/net/gaia/oauth2_mint_token_consumer.h" +#include "chrome/common/net/gaia/oauth2_mint_token_fetcher.h" +#include "content/public/common/url_fetcher.h" +#include "content/public/common/url_fetcher_delegate.h" + +class GoogleServiceAuthError; +class OAuth2MintTokenFlowTest; + +namespace net { +class URLRequestContextGetter; +} + +// Base calss 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 content::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 acces 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. + void Start(); + + // OAuth2AccessTokenFetcher implementation. + virtual void OnGetTokenSuccess(const std::string& access_token) OVERRIDE; + virtual void OnGetTokenFailure(const GoogleServiceAuthError& error) OVERRIDE; + + // content::URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const content::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 appropriate observer interface by implementing + // these template methods. + // Called when the API call finished successfully. + virtual void ProcessApiCallSuccess(const content::URLFetcher* source) = 0; + // Called when the API call failed. + virtual void ProcessApiCallFailure(const content::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: + // The steps this class performs are: + // 1. Try existing access token. + // 2. If that works, flow is done. If not, generate a new access token. + // 3. Try using new access token. + enum State { + INITIAL, + API_CALL_STARTED, + API_CALL_DONE, + MINT_ACCESS_TOKEN_STARTED, + MINT_ACCESS_TOKEN_DONE, + ERROR_STATE + }; + + friend class OAuth2ApiCallFlowTest; + + // Helper to create an instnace of access token fetcher. + // Caller owns the returned instance. + virtual OAuth2AccessTokenFetcher* CreateAccessTokenFetcher(); + + // Creates an instance of URLFetcher that does not send or save cookies. + // The URLFether's method will be GET if body is empty, POST otherwise. + // Caller owns the returned instance. + virtual content::URLFetcher* CreateURLFetcher(); + + // Helper methods to implement the state machien for the flow. + void BeginApiCall(); + void EndApiCall(const content::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 access token once. + bool tried_mint_access_token_; + + scoped_ptr<content::URLFetcher> url_fetcher_; + scoped_ptr<OAuth2AccessTokenFetcher> oauth2_access_token_fetcher_; + + DISALLOW_COPY_AND_ASSIGN(OAuth2ApiCallFlow); +}; + +#endif // CHROME_COMMON_NET_GAIA_OAUTH2_API_CALL_FLOW_H_ diff --git a/chrome/common/net/gaia/oauth2_api_call_flow_unittest.cc b/chrome/common/net/gaia/oauth2_api_call_flow_unittest.cc new file mode 100644 index 0000000..b72335f --- /dev/null +++ b/chrome/common/net/gaia/oauth2_api_call_flow_unittest.cc @@ -0,0 +1,263 @@ +// 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 "chrome/common/net/gaia/gaia_urls.h" +#include "chrome/common/net/gaia/google_service_auth_error.h" +#include "chrome/common/net/gaia/oauth2_access_token_consumer.h" +#include "chrome/common/net/gaia/oauth2_access_token_fetcher.h" +#include "chrome/common/net/gaia/oauth2_api_call_flow.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_url_fetcher_factory.h" +#include "net/http/http_status_code.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::URLFetcher; +using content::URLFetcherDelegate; +using content::URLFetcherFactory; +using net::URLRequestStatus; +using testing::_; +using testing::Return; + +namespace { +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 content::URLFetcher* source)); + MOCK_METHOD1(ProcessApiCallFailure, + void (const content::URLFetcher* source)); + MOCK_METHOD1(ProcessNewAccessToken, + void (const std::string& access_token)); + MOCK_METHOD1(ProcessMintAccessTokenFailure, + void (const GoogleServiceAuthError& error)); + MOCK_METHOD0(CreateAccessTokenFetcher, OAuth2AccessTokenFetcher* ()); + // MOCK_METHOD0(CreateURLFetcher, URLFetcher* ()); +}; + +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) { + GURL url(CreateApiUrl()); + EXPECT_CALL(*flow_, CreateApiCallBody()).WillOnce(Return("")); + 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); + 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); + 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); + 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); + 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); +} |