From 271398aa3ff859d3deace9ff3ca575f14d263964 Mon Sep 17 00:00:00 2001 From: "munjal@chromium.org" Date: Thu, 1 Mar 2012 21:26:18 +0000 Subject: Add code to mint OAuth tokens from login-scoped OAuth token. - Add a fetcher class, OAuth2TokenMintFetcher, to abstract OAuth2 token mint process. - Add tests. The work to use this in AppNotifyChannlesetup will be done in upcoming patch. Review URL: https://chromiumcodereview.appspot.com/9549013 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@124486 0039d316-1c4b-4281-b951-d872f2087c98 --- chrome/common/net/gaia/gaia_urls.cc | 7 + chrome/common/net/gaia/gaia_urls.h | 2 + .../common/net/gaia/oauth2_mint_token_consumer.h | 23 +++ .../common/net/gaia/oauth2_mint_token_fetcher.cc | 189 +++++++++++++++++++++ chrome/common/net/gaia/oauth2_mint_token_fetcher.h | 105 ++++++++++++ .../net/gaia/oauth2_mint_token_fetcher_unittest.cc | 175 +++++++++++++++++++ 6 files changed, 501 insertions(+) create mode 100644 chrome/common/net/gaia/oauth2_mint_token_consumer.h create mode 100644 chrome/common/net/gaia/oauth2_mint_token_fetcher.cc create mode 100644 chrome/common/net/gaia/oauth2_mint_token_fetcher.h create mode 100644 chrome/common/net/gaia/oauth2_mint_token_fetcher_unittest.cc (limited to 'chrome/common') diff --git a/chrome/common/net/gaia/gaia_urls.cc b/chrome/common/net/gaia/gaia_urls.cc index 49a33c0..affc914 100644 --- a/chrome/common/net/gaia/gaia_urls.cc +++ b/chrome/common/net/gaia/gaia_urls.cc @@ -38,6 +38,8 @@ 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"; } // namespacce GaiaUrls* GaiaUrls::GetInstance() { @@ -80,6 +82,7 @@ GaiaUrls::GaiaUrls() { oauth2_chrome_client_secret_ = kOAuth2ChromeClientSecret; client_login_to_oauth2_url_ = kClientLoginToOAuth2Url; oauth2_token_url_ = kOAuth2TokenUrl; + oauth2_issue_token_url_ = kOAuth2IssueTokenUrl; } GaiaUrls::~GaiaUrls() { @@ -160,3 +163,7 @@ const std::string& GaiaUrls::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_; +} diff --git a/chrome/common/net/gaia/gaia_urls.h b/chrome/common/net/gaia/gaia_urls.h index c8c1b46..c64b078 100644 --- a/chrome/common/net/gaia/gaia_urls.h +++ b/chrome/common/net/gaia/gaia_urls.h @@ -38,6 +38,7 @@ class GaiaUrls { 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(); private: GaiaUrls(); @@ -67,6 +68,7 @@ class GaiaUrls { std::string oauth2_chrome_client_secret_; std::string client_login_to_oauth2_url_; std::string oauth2_token_url_; + std::string oauth2_issue_token_url_; DISALLOW_COPY_AND_ASSIGN(GaiaUrls); }; diff --git a/chrome/common/net/gaia/oauth2_mint_token_consumer.h b/chrome/common/net/gaia/oauth2_mint_token_consumer.h new file mode 100644 index 0000000..a5e3647 --- /dev/null +++ b/chrome/common/net/gaia/oauth2_mint_token_consumer.h @@ -0,0 +1,23 @@ +// 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_MINT_TOKEN_CONSUMER_H_ +#define CHROME_COMMON_NET_GAIA_OAUTH2_MINT_TOKEN_CONSUMER_H_ +#pragma once + +#include + +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 // CHROME_COMMON_NET_GAIA_OAUTH2_MINT_TOKEN_CONSUMER_H_ diff --git a/chrome/common/net/gaia/oauth2_mint_token_fetcher.cc b/chrome/common/net/gaia/oauth2_mint_token_fetcher.cc new file mode 100644 index 0000000..62a2cb1 --- /dev/null +++ b/chrome/common/net/gaia/oauth2_mint_token_fetcher.cc @@ -0,0 +1,189 @@ +// 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_mint_token_fetcher.h" + +#include +#include + +#include "base/json/json_reader.h" +#include "base/string_util.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 "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; + +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 = 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& 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 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(GoogleServiceAuthError error) { + state_ = ERROR_STATE; + consumer_->OnMintTokenFailure(error); +} + +void OAuth2MintTokenFetcher::OnURLFetchComplete(const 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& 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 URLFetcher* source, + std::string* access_token) { + CHECK(source); + CHECK(access_token); + std::string data; + source->GetResponseAsString(&data); + base::JSONReader reader; + scoped_ptr value(reader.Read(data, false)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + return false; + + DictionaryValue* dict = static_cast(value.get()); + return dict->GetString(kAccessTokenKey, access_token); +} diff --git a/chrome/common/net/gaia/oauth2_mint_token_fetcher.h b/chrome/common/net/gaia/oauth2_mint_token_fetcher.h new file mode 100644 index 0000000..8418238 --- /dev/null +++ b/chrome/common/net/gaia/oauth2_mint_token_fetcher.h @@ -0,0 +1,105 @@ +// 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_MINT_TOKEN_FETCHER_H_ +#define CHROME_COMMON_NET_GAIA_OAUTH2_MINT_TOKEN_FETCHER_H_ +#pragma once + +#include +#include + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "chrome/common/net/gaia/oauth2_mint_token_consumer.h" +#include "content/public/common/url_fetcher.h" +#include "content/public/common/url_fetcher_delegate.h" +#include "googleurl/src/gurl.h" + +class OAuth2MintTokenFetcherTest; + +namespace net { +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 content::URLFetcherDelegate { + public: + OAuth2MintTokenFetcher(OAuth2MintTokenConsumer* consumer, + net::URLRequestContextGetter* getter, + const std::string& source); + virtual ~OAuth2MintTokenFetcher(); + + // Start the flow. + void Start(const std::string& oauth_login_access_token, + const std::string& client_id, + const std::vector& scopes, + const std::string& origin); + + void CancelRequest(); + + // Implementation of content::URLFetcherDelegate + virtual void OnURLFetchComplete(const content::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 content::URLFetcher* source); + + // Helper mehtods for reporting back results. + void OnMintTokenSuccess(const std::string& access_token); + void OnMintTokenFailure(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& scopes, + const std::string& origin); + static bool ParseMintTokenResponse(const content::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 fetcher_; + std::string oauth_login_access_token_; + std::string client_id_; + std::vector scopes_; + std::string origin_; + + friend class OAuth2MintTokenFetcherTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFetcherTest, + ParseMintTokenResponse); + + DISALLOW_COPY_AND_ASSIGN(OAuth2MintTokenFetcher); +}; + +#endif // CHROME_COMMON_NET_GAIA_OAUTH2_MINT_TOKEN_FETCHER_H_ diff --git a/chrome/common/net/gaia/oauth2_mint_token_fetcher_unittest.cc b/chrome/common/net/gaia/oauth2_mint_token_fetcher_unittest.cc new file mode 100644 index 0000000..b5ee4205 --- /dev/null +++ b/chrome/common/net/gaia/oauth2_mint_token_fetcher_unittest.cc @@ -0,0 +1,175 @@ +// 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 +#include + +#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/gaia/oauth2_mint_token_consumer.h" +#include "chrome/common/net/gaia/oauth2_mint_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/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::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[] = + "{" + " \"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)); +}; + +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 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); + } +} -- cgit v1.1