diff options
author | courage@chromium.org <courage@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-17 01:12:54 +0000 |
---|---|---|
committer | courage@chromium.org <courage@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-17 01:12:54 +0000 |
commit | 1249a6e392f8f3b031ee70342ac98ec818edf7fc (patch) | |
tree | b377b9beb9e858b31fc2313f0370e0ebe89a27af | |
parent | ace9afa7a2b3b40e593dfb4744b94cb6401d8308 (diff) | |
download | chromium_src-1249a6e392f8f3b031ee70342ac98ec818edf7fc.zip chromium_src-1249a6e392f8f3b031ee70342ac98ec818edf7fc.tar.gz chromium_src-1249a6e392f8f3b031ee70342ac98ec818edf7fc.tar.bz2 |
Identity API: web-based scope approval dialogs for getAuthToken
When getAuthToken needs to prompt a user for permissions, it now uses
a web-based authorization flow instead of native dialogs.
BUG=228908
(step #4 of the bug description)
Review URL: https://chromiumcodereview.appspot.com/15148007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@200683 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/extensions/api/identity/gaia_web_auth_flow.cc | 160 | ||||
-rw-r--r-- | chrome/browser/extensions/api/identity/gaia_web_auth_flow.h | 97 | ||||
-rw-r--r-- | chrome/browser/extensions/api/identity/gaia_web_auth_flow_unittest.cc | 235 | ||||
-rw-r--r-- | chrome/browser/extensions/api/identity/identity_api.cc | 77 | ||||
-rw-r--r-- | chrome/browser/extensions/api/identity/identity_api.h | 25 | ||||
-rw-r--r-- | chrome/browser/extensions/api/identity/identity_apitest.cc | 242 | ||||
-rw-r--r-- | chrome/browser/extensions/api/identity/web_auth_flow.cc | 15 | ||||
-rw-r--r-- | chrome/browser/extensions/api/identity/web_auth_flow.h | 2 | ||||
-rw-r--r-- | chrome/chrome_browser_extensions.gypi | 2 | ||||
-rw-r--r-- | chrome/chrome_tests_unit.gypi | 1 | ||||
-rw-r--r-- | google_apis/gaia/gaia_urls.cc | 6 | ||||
-rw-r--r-- | google_apis/gaia/gaia_urls.h | 2 |
12 files changed, 736 insertions, 128 deletions
diff --git a/chrome/browser/extensions/api/identity/gaia_web_auth_flow.cc b/chrome/browser/extensions/api/identity/gaia_web_auth_flow.cc new file mode 100644 index 0000000..9362b3b --- /dev/null +++ b/chrome/browser/extensions/api/identity/gaia_web_auth_flow.cc @@ -0,0 +1,160 @@ +// Copyright (c) 2013 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/browser/extensions/api/identity/gaia_web_auth_flow.h" + +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/base/escape.h" + +namespace extensions { + +GaiaWebAuthFlow::GaiaWebAuthFlow(Delegate* delegate, + Profile* profile, + chrome::HostDesktopType host_desktop_type, + const std::string& extension_id, + const OAuth2Info& oauth2_info) + : delegate_(delegate), + profile_(profile), + host_desktop_type_(host_desktop_type) { + const char kOAuth2RedirectPathFormat[] = "/%s#"; + const char kOAuth2AuthorizeFormat[] = + "%s?response_type=token&approval_prompt=force&authuser=0&" + "client_id=%s&" + "scope=%s&" + "origin=chrome-extension://%s/&" + "redirect_uri=%s:/%s"; + + std::vector<std::string> client_id_parts; + base::SplitString(oauth2_info.client_id, '.', &client_id_parts); + std::reverse(client_id_parts.begin(), client_id_parts.end()); + redirect_scheme_ = JoinString(client_id_parts, '.'); + + redirect_path_prefix_ = + base::StringPrintf(kOAuth2RedirectPathFormat, extension_id.c_str()); + + auth_url_ = GURL(base::StringPrintf( + kOAuth2AuthorizeFormat, + GaiaUrls::GetInstance()->oauth2_auth_url().c_str(), + oauth2_info.client_id.c_str(), + net::EscapeUrlEncodedData(JoinString(oauth2_info.scopes, ' '), true) + .c_str(), + extension_id.c_str(), + redirect_scheme_.c_str(), + extension_id.c_str())); +} + +GaiaWebAuthFlow::~GaiaWebAuthFlow() {} + +void GaiaWebAuthFlow::Start() { + ubertoken_fetcher_.reset(new UbertokenFetcher(profile_, this)); + ubertoken_fetcher_->StartFetchingToken(); +} + +void GaiaWebAuthFlow::OnUbertokenSuccess(const std::string& token) { + const char kMergeSessionQueryFormat[] = "?uberauth=%s&" + "continue=%s&" + "source=appsv2"; + + std::string merge_query = base::StringPrintf( + kMergeSessionQueryFormat, + net::EscapeUrlEncodedData(token, true).c_str(), + net::EscapeUrlEncodedData(auth_url_.spec(), true).c_str()); + GURL merge_url(GaiaUrls::GetInstance()->merge_session_url() + merge_query); + + web_flow_ = CreateWebAuthFlow(merge_url); + web_flow_->Start(); +} + +void GaiaWebAuthFlow::OnUbertokenFailure(const GoogleServiceAuthError& error) { + delegate_->OnGaiaFlowFailure( + GaiaWebAuthFlow::SERVICE_AUTH_ERROR, error, std::string()); +} + +void GaiaWebAuthFlow::OnAuthFlowFailure(WebAuthFlow::Failure failure) { + DCHECK(failure == WebAuthFlow::WINDOW_CLOSED); + delegate_->OnGaiaFlowFailure( + GaiaWebAuthFlow::WINDOW_CLOSED, + GoogleServiceAuthError(GoogleServiceAuthError::NONE), + std::string()); +} + +void GaiaWebAuthFlow::OnAuthFlowURLChange(const GURL& url) { + const char kOAuth2RedirectAccessTokenKey[] = "access_token"; + const char kOAuth2RedirectErrorKey[] = "error"; + const char kOAuth2ExpiresInKey[] = "expires_in"; + + // The format of the target URL is: + // reversed.oauth.client.id:/extensionid#access_token=TOKEN + // + // Because there is no double slash, everything after the scheme is + // interpreted as a path, including the fragment. + + if (url.scheme() == redirect_scheme_ && !url.has_host() && !url.has_port() && + StartsWithASCII(url.path(), redirect_path_prefix_, true)) { + web_flow_.reset(); + + std::string fragment = + url.path().substr(redirect_path_prefix_.length(), std::string::npos); + std::vector<std::pair<std::string, std::string> > pairs; + base::SplitStringIntoKeyValuePairs(fragment, '=', '&', &pairs); + std::string access_token; + std::string error; + std::string expiration; + + for (std::vector<std::pair<std::string, std::string> >::iterator + it = pairs.begin(); + it != pairs.end(); + ++it) { + if (it->first == kOAuth2RedirectAccessTokenKey) + access_token = it->second; + else if (it->first == kOAuth2RedirectErrorKey) + error = it->second; + else if (it->first == kOAuth2ExpiresInKey) + expiration = it->second; + } + + if (access_token.empty() && error.empty()) { + delegate_->OnGaiaFlowFailure( + GaiaWebAuthFlow::INVALID_REDIRECT, + GoogleServiceAuthError(GoogleServiceAuthError::NONE), + std::string()); + } else if (!error.empty()) { + delegate_->OnGaiaFlowFailure( + GaiaWebAuthFlow::OAUTH_ERROR, + GoogleServiceAuthError(GoogleServiceAuthError::NONE), + error); + } else { + delegate_->OnGaiaFlowCompleted(access_token, expiration); + } + } +} + +void GaiaWebAuthFlow::OnAuthFlowTitleChange(const std::string& title) { + // On the final page the title will be "Loading <redirect-url>". + // Treat it as though we'd really been redirected to <redirect-url>. + const char kRedirectPrefix[] = "Loading "; + std::string prefix(kRedirectPrefix); + + if (StartsWithASCII(title, prefix, true)) { + GURL url(title.substr(prefix.length(), std::string::npos)); + if (url.is_valid()) + OnAuthFlowURLChange(url); + } +} + +scoped_ptr<WebAuthFlow> GaiaWebAuthFlow::CreateWebAuthFlow(GURL url) { + gfx::Rect initial_bounds; + return scoped_ptr<WebAuthFlow>(new WebAuthFlow(this, + profile_, + url, + WebAuthFlow::INTERACTIVE, + initial_bounds, + host_desktop_type_)); +} + +} // extensions diff --git a/chrome/browser/extensions/api/identity/gaia_web_auth_flow.h b/chrome/browser/extensions/api/identity/gaia_web_auth_flow.h new file mode 100644 index 0000000..8f9dba7 --- /dev/null +++ b/chrome/browser/extensions/api/identity/gaia_web_auth_flow.h @@ -0,0 +1,97 @@ +// Copyright (c) 2013 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_BROWSER_EXTENSIONS_API_IDENTITY_GAIA_WEB_AUTH_FLOW_H_ +#define CHROME_BROWSER_EXTENSIONS_API_IDENTITY_GAIA_WEB_AUTH_FLOW_H_ + +#include "chrome/browser/extensions/api/identity/web_auth_flow.h" +#include "chrome/browser/signin/ubertoken_fetcher.h" +#include "chrome/browser/ui/host_desktop.h" +#include "chrome/common/extensions/api/identity/oauth2_manifest_handler.h" + +namespace extensions { + +// Implements a web-based OAuth2 scope approval dialog. This flow has +// four parts: +// 1. Fetch an ubertoken for the signed-in user. +// 2. Use the ubertoken to get session cookies using MergeSession. +// 3. Start the OAuth flow and wait for final redirect. +// 4. Parse results from the fragment component of the final redirect URI. +// +// The OAuth flow is a special version of the OAuth2 out-of-band flow +// where the final response page's title contains the +// redirect_uri. The redirect URI has an unusual format to prevent its +// use in other contexts. The scheme of the URI is a reversed version +// of the OAuth client ID, and the path starts with the Chrome +// extension ID. For example, an app with the OAuth client ID +// "32610281651.apps.googleusercontent.com" and a Chrome app ID +// "kbinjhdkhikmpjoejcfofghmjjpidcnj", would get redirected to: +// +// com.googleusercontent.apps.32610281651:/kbinjhdkhikmpjoejcfofghmjjpidcnj +// +// Arriving at this URI completes the flow. The last response from +// gaia does a JavaScript redirect to the special URI, but also +// includes the same URI in its title. The navigation to this URI gets +// filtered out because of its unusual protocol scheme, so +// GaiaWebAuthFlow pulls it out of the window title instead. + +class GaiaWebAuthFlow : public UbertokenConsumer, public WebAuthFlow::Delegate { + public: + enum Failure { + WINDOW_CLOSED, // Window closed by user. + INVALID_REDIRECT, // Redirect parse error. + SERVICE_AUTH_ERROR, // Non-OAuth related authentication error + OAUTH_ERROR // Flow reached final redirect, which contained an error. + }; + + class Delegate { + public: + // Called when the flow fails prior to the final OAuth redirect, + virtual void OnGaiaFlowFailure(Failure failure, + GoogleServiceAuthError service_error, + const std::string& oauth_error) = 0; + // Called when the OAuth2 flow completes. + virtual void OnGaiaFlowCompleted(const std::string& access_token, + const std::string& expiration) = 0; + }; + + GaiaWebAuthFlow(Delegate* delegate, + Profile* profile, + chrome::HostDesktopType host_desktop_type, + const std::string& extension_id, + const OAuth2Info& oauth2_info); + virtual ~GaiaWebAuthFlow(); + + // Starts the flow by fetching an ubertoken. Can override for testing. + virtual void Start(); + + // UbertokenConsumer implementation: + virtual void OnUbertokenSuccess(const std::string& token) OVERRIDE; + virtual void OnUbertokenFailure(const GoogleServiceAuthError& error) OVERRIDE; + + // WebAuthFlow::Delegate implementation. + virtual void OnAuthFlowFailure(WebAuthFlow::Failure failure) OVERRIDE; + virtual void OnAuthFlowURLChange(const GURL& redirect_url) OVERRIDE; + virtual void OnAuthFlowTitleChange(const std::string& title) OVERRIDE; + + private: + // Creates a WebAuthFlow, which will navigate to |url|. Can override + // for testing. Used to kick off the MergeSession (step #2). + virtual scoped_ptr<WebAuthFlow> CreateWebAuthFlow(GURL url); + + Delegate* delegate_; + Profile* profile_; + chrome::HostDesktopType host_desktop_type_; + std::string redirect_scheme_; + std::string redirect_path_prefix_; + GURL auth_url_; + scoped_ptr<UbertokenFetcher> ubertoken_fetcher_; + scoped_ptr<WebAuthFlow> web_flow_; + + DISALLOW_COPY_AND_ASSIGN(GaiaWebAuthFlow); +}; + +} // extensions + +#endif // CHROME_BROWSER_EXTENSIONS_API_IDENTITY_GAIA_WEB_AUTH_FLOW_H_ diff --git a/chrome/browser/extensions/api/identity/gaia_web_auth_flow_unittest.cc b/chrome/browser/extensions/api/identity/gaia_web_auth_flow_unittest.cc new file mode 100644 index 0000000..77c7fc04 --- /dev/null +++ b/chrome/browser/extensions/api/identity/gaia_web_auth_flow_unittest.cc @@ -0,0 +1,235 @@ +// Copyright (c) 2013 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/browser/extensions/api/identity/gaia_web_auth_flow.h" + +#include <vector> + +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace extensions { + +class FakeWebAuthFlow : public WebAuthFlow { + public: + explicit FakeWebAuthFlow(WebAuthFlow::Delegate* delegate) + : WebAuthFlow(delegate, + NULL, + GURL(), + WebAuthFlow::INTERACTIVE, + gfx::Rect(), + chrome::GetActiveDesktop()) {} + + virtual void Start() OVERRIDE {} +}; + +class TestGaiaWebAuthFlow : public GaiaWebAuthFlow { + public: + TestGaiaWebAuthFlow(GaiaWebAuthFlow::Delegate* delegate, + const std::string& extension_id, + const OAuth2Info& oauth2_info, + GoogleServiceAuthError::State ubertoken_error_state) + : GaiaWebAuthFlow(delegate, + NULL, + chrome::GetActiveDesktop(), + "extension_id", + oauth2_info), + ubertoken_error_(ubertoken_error_state) {} + + virtual void Start() OVERRIDE { + if (ubertoken_error_.state() == GoogleServiceAuthError::NONE) + OnUbertokenSuccess("fake_ubertoken"); + else + OnUbertokenFailure(ubertoken_error_); + } + + private: + virtual scoped_ptr<WebAuthFlow> CreateWebAuthFlow(GURL url) OVERRIDE { + return scoped_ptr<WebAuthFlow>(new FakeWebAuthFlow(this)); + } + + GoogleServiceAuthError ubertoken_error_; +}; + +class MockGaiaWebAuthFlowDelegate : public GaiaWebAuthFlow::Delegate { + public: + MOCK_METHOD3(OnGaiaFlowFailure, + void(GaiaWebAuthFlow::Failure failure, + GoogleServiceAuthError service_error, + const std::string& oauth_error)); + MOCK_METHOD2(OnGaiaFlowCompleted, + void(const std::string& access_token, + const std::string& expiration)); +}; + +class IdentityGaiaWebAuthFlowTest : public testing::Test { + public: + IdentityGaiaWebAuthFlowTest() + : ubertoken_error_state_(GoogleServiceAuthError::NONE) {} + + scoped_ptr<TestGaiaWebAuthFlow> CreateTestFlow() { + OAuth2Info oauth2_info; + oauth2_info.client_id = "fake.client.id"; + return scoped_ptr<TestGaiaWebAuthFlow>(new TestGaiaWebAuthFlow( + &delegate_, "extension_id", oauth2_info, ubertoken_error_state_)); + + } + + std::string GetFinalTitle(const std::string& fragment) { + return std::string("Loading id.client.fake:/extension_id#") + fragment; + } + + GoogleServiceAuthError GetNoneServiceError() { + return GoogleServiceAuthError(GoogleServiceAuthError::NONE); + } + + void set_ubertoken_error( + GoogleServiceAuthError::State ubertoken_error_state) { + ubertoken_error_state_ = ubertoken_error_state; + } + + protected: + testing::StrictMock<MockGaiaWebAuthFlowDelegate> delegate_; + GoogleServiceAuthError::State ubertoken_error_state_; +}; + +TEST_F(IdentityGaiaWebAuthFlowTest, OAuthError) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + EXPECT_CALL(delegate_, OnGaiaFlowFailure( + GaiaWebAuthFlow::OAUTH_ERROR, + GoogleServiceAuthError(GoogleServiceAuthError::NONE), + "access_denied")); + flow->OnAuthFlowTitleChange(GetFinalTitle("error=access_denied")); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, Token) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + EXPECT_CALL(delegate_, OnGaiaFlowCompleted("fake_access_token", "")); + flow->OnAuthFlowTitleChange(GetFinalTitle("access_token=fake_access_token")); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, TokenAndExpiration) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + EXPECT_CALL(delegate_, OnGaiaFlowCompleted("fake_access_token", "3600")); + flow->OnAuthFlowTitleChange( + GetFinalTitle("access_token=fake_access_token&expires_in=3600")); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, ExtraFragmentParametersSuccess) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + EXPECT_CALL(delegate_, + OnGaiaFlowCompleted("fake_access_token", "3600")); + flow->OnAuthFlowTitleChange(GetFinalTitle("chaff1=stuff&" + "expires_in=3600&" + "chaff2=and&" + "nonerror=fake_error&" + "chaff3=nonsense&" + "access_token=fake_access_token&" + "chaff4=")); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, ExtraFragmentParametersError) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + EXPECT_CALL(delegate_, OnGaiaFlowFailure( + GaiaWebAuthFlow::OAUTH_ERROR, + GoogleServiceAuthError(GoogleServiceAuthError::NONE), + "fake_error")); + flow->OnAuthFlowTitleChange(GetFinalTitle("chaff1=stuff&" + "expires_in=3600&" + "chaff2=and&" + "error=fake_error&" + "chaff3=nonsense&" + "access_token=fake_access_token&" + "chaff4=")); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, TitleSpam) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + flow->OnAuthFlowTitleChange( + "Loading https://extension_id.chromiumapp.org/#error=non_final_title"); + flow->OnAuthFlowTitleChange("I'm feeling entitled."); + flow->OnAuthFlowTitleChange(""); + flow->OnAuthFlowTitleChange( + "Loading id.client.fake:/bad_extension_id#error=non_final_title"); + flow->OnAuthFlowTitleChange( + "Loading bad.id.client.fake:/extension_id#error=non_final_title"); + EXPECT_CALL(delegate_, OnGaiaFlowCompleted("fake_access_token", "")); + flow->OnAuthFlowTitleChange(GetFinalTitle("access_token=fake_access_token")); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, EmptyFragment) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + EXPECT_CALL( + delegate_, + OnGaiaFlowFailure( + GaiaWebAuthFlow::INVALID_REDIRECT, + GoogleServiceAuthError(GoogleServiceAuthError::NONE), + "")); + flow->OnAuthFlowTitleChange(GetFinalTitle("")); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, JunkFragment) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + EXPECT_CALL( + delegate_, + OnGaiaFlowFailure( + GaiaWebAuthFlow::INVALID_REDIRECT, + GoogleServiceAuthError(GoogleServiceAuthError::NONE), + "")); + flow->OnAuthFlowTitleChange(GetFinalTitle("thisisjustabunchofjunk")); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, NoFragment) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + // This won't be recognized as an interesting title. + flow->OnAuthFlowTitleChange("Loading id.client.fake:/extension_id"); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, Host) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + // These won't be recognized as interesting titles. + flow->OnAuthFlowTitleChange( + "Loading id.client.fake://extension_id#access_token=fake_access_token"); + flow->OnAuthFlowTitleChange( + "Loading id.client.fake://extension_id/#access_token=fake_access_token"); + flow->OnAuthFlowTitleChange( + "Loading " + "id.client.fake://host/extension_id/#access_token=fake_access_token"); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, UbertokenFailure) { + set_ubertoken_error(GoogleServiceAuthError::CONNECTION_FAILED); + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + EXPECT_CALL( + delegate_, + OnGaiaFlowFailure( + GaiaWebAuthFlow::SERVICE_AUTH_ERROR, + GoogleServiceAuthError(GoogleServiceAuthError::CONNECTION_FAILED), + "")); + flow->Start(); +} + +TEST_F(IdentityGaiaWebAuthFlowTest, AuthFlowFailure) { + scoped_ptr<TestGaiaWebAuthFlow> flow = CreateTestFlow(); + flow->Start(); + EXPECT_CALL( + delegate_, + OnGaiaFlowFailure( + GaiaWebAuthFlow::WINDOW_CLOSED, + GoogleServiceAuthError(GoogleServiceAuthError::NONE), + "")); + flow->OnAuthFlowFailure(WebAuthFlow::WINDOW_CLOSED); +} + +} // namespace extensions diff --git a/chrome/browser/extensions/api/identity/identity_api.cc b/chrome/browser/extensions/api/identity/identity_api.cc index bb74eb4..178e6f7 100644 --- a/chrome/browser/extensions/api/identity/identity_api.cc +++ b/chrome/browser/extensions/api/identity/identity_api.cc @@ -10,6 +10,7 @@ #include "base/lazy_instance.h" #include "base/stringprintf.h" +#include "base/strings/string_number_conversions.h" #include "base/values.h" #include "chrome/browser/app_mode/app_mode_utils.h" #include "chrome/browser/extensions/extension_function_dispatcher.h" @@ -224,7 +225,6 @@ void IdentityGetAuthTokenFunction::StartMintToken( CompleteMintTokenFlow(); CompleteFunctionWithResult(cache_entry.token()); } else { - install_ui_.reset(new ExtensionInstallPrompt(GetAssociatedWebContents())); ShowOAuthApprovalDialog(issue_advice_); } } @@ -291,15 +291,55 @@ void IdentityGetAuthTokenFunction::SigninFailed() { CompleteFunctionWithError(identity_constants::kUserNotSignedIn); } -void IdentityGetAuthTokenFunction::InstallUIProceed() { - // The user has accepted the scopes, so we may now force (recording a grant - // and receiving a token). - StartGaiaRequest(OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE); +void IdentityGetAuthTokenFunction::OnGaiaFlowFailure( + GaiaWebAuthFlow::Failure failure, + GoogleServiceAuthError service_error, + const std::string& oauth_error) { + CompleteMintTokenFlow(); + std::string error; + + switch (failure) { + case GaiaWebAuthFlow::WINDOW_CLOSED: + error = identity_constants::kUserRejected; + break; + + case GaiaWebAuthFlow::INVALID_REDIRECT: + error = identity_constants::kInvalidRedirect; + break; + + case GaiaWebAuthFlow::SERVICE_AUTH_ERROR: + error = std::string(identity_constants::kAuthFailure) + + service_error.ToString(); + break; + + case GaiaWebAuthFlow::OAUTH_ERROR: + error = MapOAuth2ErrorToDescription(oauth_error); + break; + + default: + NOTREACHED() << "Unexpected error from gaia web auth flow: " << failure; + error = identity_constants::kInvalidRedirect; + break; + } + + CompleteFunctionWithError(error); } -void IdentityGetAuthTokenFunction::InstallUIAbort(bool user_initiated) { +void IdentityGetAuthTokenFunction::OnGaiaFlowCompleted( + const std::string& access_token, + const std::string& expiration) { + + int time_to_live; + if (!expiration.empty() && base::StringToInt(expiration, &time_to_live)) { + const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension()); + IdentityTokenCacheValue token_value( + access_token, base::TimeDelta::FromSeconds(time_to_live)); + IdentityAPI::GetFactoryInstance()->GetForProfile(profile()) + ->SetCachedToken(GetExtension()->id(), oauth2_info.scopes, token_value); + } + CompleteMintTokenFlow(); - CompleteFunctionWithError(identity_constants::kUserRejected); + CompleteFunctionWithResult(access_token); } void IdentityGetAuthTokenFunction::StartGaiaRequest( @@ -315,7 +355,15 @@ void IdentityGetAuthTokenFunction::ShowLoginPopup() { void IdentityGetAuthTokenFunction::ShowOAuthApprovalDialog( const IssueAdviceInfo& issue_advice) { - install_ui_->ConfirmIssueAdvice(this, GetExtension(), issue_advice); + Browser* current_browser = this->GetCurrentBrowser(); + chrome::HostDesktopType host_desktop_type = + current_browser ? current_browser->host_desktop_type() + : chrome::GetActiveDesktop(); + const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension()); + + gaia_web_auth_flow_.reset(new GaiaWebAuthFlow( + this, profile(), host_desktop_type, GetExtension()->id(), oauth2_info)); + gaia_web_auth_flow_->Start(); } OAuth2MintTokenFlow* IdentityGetAuthTokenFunction::CreateMintTokenFlow( @@ -350,6 +398,19 @@ bool IdentityGetAuthTokenFunction::HasLoginToken() const { return token_service->HasOAuthLoginToken(); } +std::string IdentityGetAuthTokenFunction::MapOAuth2ErrorToDescription( + const std::string& error) { + const char kOAuth2ErrorAccessDenied[] = "access_denied"; + const char kOAuth2ErrorInvalidScope[] = "invalid_scope"; + + if (error == kOAuth2ErrorAccessDenied) + return std::string(identity_constants::kUserRejected); + else if (error == kOAuth2ErrorInvalidScope) + return std::string(identity_constants::kInvalidScopes); + else + return std::string(identity_constants::kAuthFailure) + error; +} + IdentityRemoveCachedAuthTokenFunction::IdentityRemoveCachedAuthTokenFunction() { } diff --git a/chrome/browser/extensions/api/identity/identity_api.h b/chrome/browser/extensions/api/identity/identity_api.h index 9fb6ddc..ed99871 100644 --- a/chrome/browser/extensions/api/identity/identity_api.h +++ b/chrome/browser/extensions/api/identity/identity_api.h @@ -11,23 +11,24 @@ #include "base/memory/ref_counted.h" #include "base/memory/weak_ptr.h" +#include "chrome/browser/extensions/api/identity/gaia_web_auth_flow.h" #include "chrome/browser/extensions/api/identity/identity_mint_queue.h" #include "chrome/browser/extensions/api/identity/identity_signin_flow.h" #include "chrome/browser/extensions/api/identity/web_auth_flow.h" #include "chrome/browser/extensions/api/profile_keyed_api_factory.h" #include "chrome/browser/extensions/extension_function.h" -#include "chrome/browser/extensions/extension_install_prompt.h" #include "chrome/browser/signin/signin_global_error.h" #include "google_apis/gaia/oauth2_mint_token_flow.h" -class GetAuthTokenFunctionTest; -class MockGetAuthTokenFunction; class GoogleServiceAuthError; class Profile; class SigninManagerBase; namespace extensions { +class GetAuthTokenFunctionTest; +class MockGetAuthTokenFunction; + namespace identity_constants { extern const char kInvalidClientId[]; extern const char kInvalidScopes[]; @@ -58,7 +59,7 @@ extern const char kOffTheRecord[]; // new login token, there is a sign-in flow. If that flow completes // successfully, getAuthToken proceeds to the non-interactive flow. class IdentityGetAuthTokenFunction : public AsyncExtensionFunction, - public ExtensionInstallPrompt::Delegate, + public GaiaWebAuthFlow::Delegate, public IdentityMintRequestQueue::Request, public OAuth2MintTokenFlow::Delegate, public IdentitySigninFlow::Delegate { @@ -102,9 +103,12 @@ class IdentityGetAuthTokenFunction : public AsyncExtensionFunction, virtual void SigninSuccess(const std::string& token) OVERRIDE; virtual void SigninFailed() OVERRIDE; - // ExtensionInstallPrompt::Delegate implementation: - virtual void InstallUIProceed() OVERRIDE; - virtual void InstallUIAbort(bool user_initiated) OVERRIDE; + // GaiaWebAuthFlow::Delegate implementation: + virtual void OnGaiaFlowFailure(GaiaWebAuthFlow::Failure failure, + GoogleServiceAuthError service_error, + const std::string& oauth_error) OVERRIDE; + virtual void OnGaiaFlowCompleted(const std::string& access_token, + const std::string& expiration) OVERRIDE; // Starts a mint token request to GAIA. void StartGaiaRequest(OAuth2MintTokenFlow::Mode mode); @@ -119,6 +123,10 @@ class IdentityGetAuthTokenFunction : public AsyncExtensionFunction, // Checks if there is a master login token to mint tokens for the extension. virtual bool HasLoginToken() const; + // Maps OAuth2 protocol errors to an error message returned to the + // developer in chrome.runtime.lastError. + std::string MapOAuth2ErrorToDescription(const std::string& error); + bool should_prompt_for_scopes_; IdentityMintRequestQueue::MintType mint_token_flow_type_; scoped_ptr<OAuth2MintTokenFlow> mint_token_flow_; @@ -128,7 +136,7 @@ class IdentityGetAuthTokenFunction : public AsyncExtensionFunction, // When launched in interactive mode, and if there is no existing grant, // a permissions prompt will be popped up to the user. IssueAdviceInfo issue_advice_; - scoped_ptr<ExtensionInstallPrompt> install_ui_; + scoped_ptr<GaiaWebAuthFlow> gaia_web_auth_flow_; scoped_ptr<IdentitySigninFlow> signin_flow_; }; @@ -163,6 +171,7 @@ class IdentityLaunchWebAuthFlowFunction : public AsyncExtensionFunction, // WebAuthFlow::Delegate implementation. virtual void OnAuthFlowFailure(WebAuthFlow::Failure failure) OVERRIDE; virtual void OnAuthFlowURLChange(const GURL& redirect_url) OVERRIDE; + virtual void OnAuthFlowTitleChange(const std::string& title) OVERRIDE {} // Helper to initialize final URL prefix. void InitFinalRedirectURLPrefix(const std::string& extension_id); diff --git a/chrome/browser/extensions/api/identity/identity_apitest.cc b/chrome/browser/extensions/api/identity/identity_apitest.cc index ec97e685c..3dc060c 100644 --- a/chrome/browser/extensions/api/identity/identity_apitest.cc +++ b/chrome/browser/extensions/api/identity/identity_apitest.cc @@ -193,25 +193,32 @@ ProfileKeyedService* IdentityAPITestFactory(content::BrowserContext* profile) { class MockGetAuthTokenFunction : public IdentityGetAuthTokenFunction { public: MockGetAuthTokenFunction() : login_ui_result_(true), - install_ui_result_(false), + scope_ui_result_(true), login_ui_shown_(false), - install_ui_shown_(false) { + scope_ui_shown_(false) { } void set_login_ui_result(bool result) { login_ui_result_ = result; } - void set_install_ui_result(bool result) { - install_ui_result_ = result; + void set_scope_ui_failure(GaiaWebAuthFlow::Failure failure) { + scope_ui_result_ = false; + scope_ui_failure_ = failure; + } + + void set_scope_ui_oauth_error(const std::string& oauth_error) { + scope_ui_result_ = false; + scope_ui_failure_ = GaiaWebAuthFlow::OAUTH_ERROR; + scope_ui_oauth_error_ = oauth_error; } bool login_ui_shown() const { return login_ui_shown_; } - bool install_ui_shown() const { - return install_ui_shown_; + bool scope_ui_shown() const { + return scope_ui_shown_; } virtual void ShowLoginPopup() OVERRIDE { @@ -225,12 +232,17 @@ class MockGetAuthTokenFunction : public IdentityGetAuthTokenFunction { virtual void ShowOAuthApprovalDialog( const IssueAdviceInfo& issue_advice) OVERRIDE { - install_ui_shown_ = true; - // Call InstallUIProceed or InstallUIAbort based on the flag. - if (install_ui_result_) - InstallUIProceed(); - else - InstallUIAbort(true); + scope_ui_shown_ = true; + + if (scope_ui_result_) { + OnGaiaFlowCompleted(kAccessToken, "3600"); + } else if (scope_ui_failure_ == GaiaWebAuthFlow::SERVICE_AUTH_ERROR) { + GoogleServiceAuthError error(GoogleServiceAuthError::CONNECTION_FAILED); + OnGaiaFlowFailure(scope_ui_failure_, error, ""); + } else { + GoogleServiceAuthError error(GoogleServiceAuthError::NONE); + OnGaiaFlowFailure(scope_ui_failure_, error, scope_ui_oauth_error_); + } } MOCK_CONST_METHOD0(HasLoginToken, bool()); @@ -240,9 +252,11 @@ class MockGetAuthTokenFunction : public IdentityGetAuthTokenFunction { private: ~MockGetAuthTokenFunction() {} bool login_ui_result_; - bool install_ui_result_; + bool scope_ui_result_; + GaiaWebAuthFlow::Failure scope_ui_failure_; + std::string scope_ui_oauth_error_; bool login_ui_shown_; - bool install_ui_shown_; + bool scope_ui_shown_; }; class MockQueuedMintRequest : public IdentityMintRequestQueue::Request { @@ -290,7 +304,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{}]", browser()); EXPECT_EQ(std::string(errors::kInvalidClientId), error); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -301,7 +315,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{}]", browser()); EXPECT_EQ(std::string(errors::kInvalidScopes), error); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -313,7 +327,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{}]", browser()); EXPECT_EQ(std::string(errors::kUserNotSignedIn), error); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -329,7 +343,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{}]", browser()); EXPECT_TRUE(StartsWithASCII(error, errors::kAuthFailure, false)); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -346,7 +360,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{}]", browser()); EXPECT_EQ(std::string(errors::kNoGrant), error); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension); EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_ADVICE, @@ -367,7 +381,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{}]", browser()); EXPECT_TRUE(StartsWithASCII(error, errors::kAuthFailure, false)); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -387,7 +401,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, EXPECT_TRUE(value->GetAsString(&access_token)); EXPECT_EQ(std::string(kAccessToken), access_token); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_TOKEN, id_api()->GetCachedToken(extension->id(), oauth2_info.scopes).status()); @@ -403,7 +417,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{\"interactive\": true}]", browser()); EXPECT_EQ(std::string(errors::kUserNotSignedIn), error); EXPECT_TRUE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -420,7 +434,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{\"interactive\": true}]", browser()); EXPECT_EQ(std::string(errors::kUserNotSignedIn), error); EXPECT_TRUE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -433,7 +447,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{\"interactive\": true}]", browser()); EXPECT_EQ(std::string(errors::kUserNotSignedIn), error); EXPECT_TRUE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -450,7 +464,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{\"interactive\": true}]", browser()); EXPECT_TRUE(StartsWithASCII(error, errors::kAuthFailure, false)); EXPECT_TRUE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -469,7 +483,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, EXPECT_TRUE(value->GetAsString(&access_token)); EXPECT_EQ(std::string(kAccessToken), access_token); EXPECT_TRUE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -482,65 +496,34 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow( TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)).WillOnce(Return(flow)); - func->set_install_ui_result(false); + func->set_scope_ui_failure(GaiaWebAuthFlow::WINDOW_CLOSED); std::string error = utils::RunFunctionAndReturnError( func.get(), "[{\"interactive\": true}]", browser()); EXPECT_EQ(std::string(errors::kUserRejected), error); EXPECT_TRUE(func->login_ui_shown()); - EXPECT_TRUE(func->install_ui_shown()); -} - -IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, - InteractiveLoginSuccessApprovalDoneMintFailure) { - scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction()); - func->set_extension(CreateExtension(CLIENT_ID | SCOPES)); - EXPECT_CALL(*func.get(), HasLoginToken()) - .WillOnce(Return(false)); - func->set_login_ui_result(true); - TestOAuth2MintTokenFlow* flow1 = new TestOAuth2MintTokenFlow( - TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); - TestOAuth2MintTokenFlow* flow2 = new TestOAuth2MintTokenFlow( - TestOAuth2MintTokenFlow::MINT_TOKEN_FAILURE, func.get()); - EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)) - .WillOnce(Return(flow1)) - .WillOnce(Return(flow2)); - - func->set_install_ui_result(true); - std::string error = utils::RunFunctionAndReturnError( - func.get(), "[{\"interactive\": true}]", browser()); - EXPECT_TRUE(StartsWithASCII(error, errors::kAuthFailure, false)); - EXPECT_TRUE(func->login_ui_shown()); - EXPECT_TRUE(func->install_ui_shown()); + EXPECT_TRUE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, - InteractiveLoginSuccessApprovalDoneMintSuccess) { - scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction()); + InteractiveLoginSuccessApprovalSuccess) { scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES)); + scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction()); func->set_extension(extension); - const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension); EXPECT_CALL(*func.get(), HasLoginToken()) .WillOnce(Return(false)); func->set_login_ui_result(true); - TestOAuth2MintTokenFlow* flow1 = new TestOAuth2MintTokenFlow( + TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow( TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); - TestOAuth2MintTokenFlow* flow2 = new TestOAuth2MintTokenFlow( - TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS, func.get()); EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)) - .WillOnce(Return(flow1)) - .WillOnce(Return(flow2)); + .WillOnce(Return(flow)); - func->set_install_ui_result(true); scoped_ptr<base::Value> value(utils::RunFunctionAndReturnSingleResult( func.get(), "[{\"interactive\": true}]", browser())); std::string access_token; EXPECT_TRUE(value->GetAsString(&access_token)); EXPECT_EQ(std::string(kAccessToken), access_token); EXPECT_TRUE(func->login_ui_shown()); - EXPECT_TRUE(func->install_ui_shown()); - EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_TOKEN, - id_api()->GetCachedToken(extension->id(), - oauth2_info.scopes).status()); + EXPECT_TRUE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -552,58 +535,102 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow( TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)).WillOnce(Return(flow)); - func->set_install_ui_result(false); + func->set_scope_ui_failure(GaiaWebAuthFlow::WINDOW_CLOSED); std::string error = utils::RunFunctionAndReturnError( func.get(), "[{\"interactive\": true}]", browser()); EXPECT_EQ(std::string(errors::kUserRejected), error); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_TRUE(func->install_ui_shown()); + EXPECT_TRUE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, - InteractiveApprovalDoneMintSuccess) { + InteractiveApprovalInvalidRedirect) { scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction()); func->set_extension(CreateExtension(CLIENT_ID | SCOPES)); EXPECT_CALL(*func.get(), HasLoginToken()) .WillOnce(Return(true)); - TestOAuth2MintTokenFlow* flow1 = new TestOAuth2MintTokenFlow( + TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow( TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); - TestOAuth2MintTokenFlow* flow2 = new TestOAuth2MintTokenFlow( - TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS, func.get()); - EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)) - .WillOnce(Return(flow1)) - .WillOnce(Return(flow2)); - - func->set_install_ui_result(true); - scoped_ptr<base::Value> value(utils::RunFunctionAndReturnSingleResult( - func.get(), "[{\"interactive\": true}]", browser())); - std::string access_token; - EXPECT_TRUE(value->GetAsString(&access_token)); - EXPECT_EQ(std::string(kAccessToken), access_token); + EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)).WillOnce(Return(flow)); + func->set_scope_ui_failure(GaiaWebAuthFlow::INVALID_REDIRECT); + std::string error = utils::RunFunctionAndReturnError( + func.get(), "[{\"interactive\": true}]", browser()); + EXPECT_EQ(std::string(errors::kInvalidRedirect), error); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_TRUE(func->install_ui_shown()); + EXPECT_TRUE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, - InteractiveApprovalDoneMintBadCredentials) { + InteractiveApprovalConnectionFailure) { scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction()); func->set_extension(CreateExtension(CLIENT_ID | SCOPES)); EXPECT_CALL(*func.get(), HasLoginToken()) .WillOnce(Return(true)); - TestOAuth2MintTokenFlow* flow1 = new TestOAuth2MintTokenFlow( + TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow( TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); - TestOAuth2MintTokenFlow* flow2 = new TestOAuth2MintTokenFlow( - TestOAuth2MintTokenFlow::MINT_TOKEN_BAD_CREDENTIALS, func.get()); - EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)) - .WillOnce(Return(flow1)) - .WillOnce(Return(flow2)); - - func->set_install_ui_result(true); + EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)).WillOnce(Return(flow)); + func->set_scope_ui_failure(GaiaWebAuthFlow::SERVICE_AUTH_ERROR); std::string error = utils::RunFunctionAndReturnError( func.get(), "[{\"interactive\": true}]", browser()); EXPECT_TRUE(StartsWithASCII(error, errors::kAuthFailure, false)); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_TRUE(func->install_ui_shown()); + EXPECT_TRUE(func->scope_ui_shown()); +} + +IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, + InteractiveApprovalOAuthErrors) { + scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES)); + + std::map<std::string, std::string> error_map; + error_map.insert(std::make_pair("access_denied", errors::kUserRejected)); + error_map.insert(std::make_pair("invalid_scope", errors::kInvalidScopes)); + error_map.insert(std::make_pair( + "unmapped_error", std::string(errors::kAuthFailure) + "unmapped_error")); + + for (std::map<std::string, std::string>::const_iterator + it = error_map.begin(); + it != error_map.end(); + ++it) { + scoped_refptr<MockGetAuthTokenFunction> func( + new MockGetAuthTokenFunction()); + func->set_extension(extension); + EXPECT_CALL(*func.get(), HasLoginToken()).WillOnce(Return(true)); + TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow( + TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); + ON_CALL(*func.get(), CreateMintTokenFlow(_)).WillByDefault(Return(flow)); + func->set_scope_ui_oauth_error(it->first); + std::string error = utils::RunFunctionAndReturnError( + func.get(), "[{\"interactive\": true}]", browser()); + EXPECT_EQ(it->second, error); + EXPECT_FALSE(func->login_ui_shown()); + EXPECT_TRUE(func->scope_ui_shown()); + } +} + +IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, + InteractiveApprovalSuccess) { + scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES)); + const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension); + scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction()); + func->set_extension(extension); + EXPECT_CALL(*func.get(), HasLoginToken()) + .WillOnce(Return(true)); + TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow( + TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); + EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)) + .WillOnce(Return(flow)); + + scoped_ptr<base::Value> value(utils::RunFunctionAndReturnSingleResult( + func.get(), "[{\"interactive\": true}]", browser())); + std::string access_token; + EXPECT_TRUE(value->GetAsString(&access_token)); + EXPECT_EQ(std::string(kAccessToken), access_token); + EXPECT_FALSE(func->login_ui_shown()); + EXPECT_TRUE(func->scope_ui_shown()); + + EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_TOKEN, + id_api()->GetCachedToken(extension->id(), + oauth2_info.scopes).status()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, NoninteractiveQueue) { @@ -645,7 +672,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, NoninteractiveQueue) { EXPECT_TRUE(value->GetAsString(&access_token)); EXPECT_EQ(std::string(kAccessToken), access_token); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, InteractiveQueue) { @@ -677,15 +704,10 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, InteractiveQueue) { RunFunctionAsync(func, "[{\"interactive\": true}]"); // Verify that we have fetched the login token and run the first flow. testing::Mock::VerifyAndClearExpectations(func); - EXPECT_FALSE(func->install_ui_shown()); - - // The UI will be displayed and the second flow will be created - // after the first queued request clears. - func->set_install_ui_result(true); - TestOAuth2MintTokenFlow* flow2 = new TestOAuth2MintTokenFlow( - TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS, func.get()); - EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)).WillOnce(Return(flow2)); + EXPECT_FALSE(func->scope_ui_shown()); + // The UI will be displayed and a token retrieved after the first + // queued request clears. queue->RequestComplete(type, extension->id(), scopes, &queued_request); scoped_ptr<base::Value> value(WaitForSingleResult(func)); @@ -693,7 +715,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, InteractiveQueue) { EXPECT_TRUE(value->GetAsString(&access_token)); EXPECT_EQ(std::string(kAccessToken), access_token); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_TRUE(func->install_ui_shown()); + EXPECT_TRUE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -724,7 +746,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{}]", browser()); EXPECT_EQ(std::string(errors::kNoGrant), error); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); queue->RequestComplete(type, extension->id(), scopes, &queued_request); } @@ -750,7 +772,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, EXPECT_TRUE(value->GetAsString(&access_token)); EXPECT_EQ(std::string(kAccessToken), access_token); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -772,7 +794,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, func.get(), "[{}]", browser()); EXPECT_EQ(std::string(errors::kNoGrant), error); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -816,7 +838,7 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, EXPECT_TRUE(value->GetAsString(&access_token)); EXPECT_EQ(std::string(kAccessToken), access_token); EXPECT_FALSE(func->login_ui_shown()); - EXPECT_FALSE(func->install_ui_shown()); + EXPECT_FALSE(func->scope_ui_shown()); } IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, @@ -836,22 +858,18 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, EXPECT_CALL(*func.get(), HasLoginToken()) .WillOnce(Return(false)); func->set_login_ui_result(true); - TestOAuth2MintTokenFlow* flow1 = new TestOAuth2MintTokenFlow( + TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow( TestOAuth2MintTokenFlow::ISSUE_ADVICE_SUCCESS, func.get()); - TestOAuth2MintTokenFlow* flow2 = new TestOAuth2MintTokenFlow( - TestOAuth2MintTokenFlow::MINT_TOKEN_SUCCESS, func.get()); EXPECT_CALL(*func.get(), CreateMintTokenFlow(_)) - .WillOnce(Return(flow1)) - .WillOnce(Return(flow2)); + .WillOnce(Return(flow)); - func->set_install_ui_result(true); scoped_ptr<base::Value> value(utils::RunFunctionAndReturnSingleResult( func.get(), "[{\"interactive\": true}]", browser())); std::string access_token; EXPECT_TRUE(value->GetAsString(&access_token)); EXPECT_EQ(std::string(kAccessToken), access_token); EXPECT_TRUE(func->login_ui_shown()); - EXPECT_TRUE(func->install_ui_shown()); + EXPECT_TRUE(func->scope_ui_shown()); EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_TOKEN, id_api()->GetCachedToken(extension->id(), oauth2_info.scopes).status()); diff --git a/chrome/browser/extensions/api/identity/web_auth_flow.cc b/chrome/browser/extensions/api/identity/web_auth_flow.cc index dd76b47..3f2d945 100644 --- a/chrome/browser/extensions/api/identity/web_auth_flow.cc +++ b/chrome/browser/extensions/api/identity/web_auth_flow.cc @@ -5,11 +5,13 @@ #include "chrome/browser/extensions/api/identity/web_auth_flow.h" #include "base/location.h" +#include "base/utf_string_conversions.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_navigator.h" #include "content/public/browser/load_notification_details.h" #include "content/public/browser/navigation_controller.h" +#include "content/public/browser/navigation_entry.h" #include "content/public/browser/notification_details.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/notification_types.h" @@ -72,6 +74,10 @@ void WebAuthFlow::Start() { this, content::NOTIFICATION_RESOURCE_RECEIVED_REDIRECT, content::Source<WebContents>(contents_)); + registrar_.Add( + this, + content::NOTIFICATION_WEB_CONTENTS_TITLE_UPDATED, + content::Source<WebContents>(contents_)); controller->LoadURL( provider_url_, @@ -129,6 +135,15 @@ void WebAuthFlow::Observe(int type, BeforeUrlLoaded(redirect_details->new_url); } break; + case content::NOTIFICATION_WEB_CONTENTS_TITLE_UPDATED: { + std::pair<content::NavigationEntry*, bool>* title = + content::Details< + std::pair<content::NavigationEntry*, bool> >(details).ptr(); + + if (title->first) + delegate_->OnAuthFlowTitleChange(UTF16ToUTF8(title->first->GetTitle())); + } + break; default: NOTREACHED() << "Got a notification that we did not register for: " << type; diff --git a/chrome/browser/extensions/api/identity/web_auth_flow.h b/chrome/browser/extensions/api/identity/web_auth_flow.h index dd5eed8..882bef8 100644 --- a/chrome/browser/extensions/api/identity/web_auth_flow.h +++ b/chrome/browser/extensions/api/identity/web_auth_flow.h @@ -58,6 +58,8 @@ class WebAuthFlow : public content::NotificationObserver, // Called on redirects and other navigations to see if the URL should stop // the flow. virtual void OnAuthFlowURLChange(const GURL& redirect_url) = 0; + // Called when the title of the current page changes. + virtual void OnAuthFlowTitleChange(const std::string& title) = 0; protected: virtual ~Delegate() {} diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi index 5d39802..e08e6e9 100644 --- a/chrome/chrome_browser_extensions.gypi +++ b/chrome/chrome_browser_extensions.gypi @@ -235,6 +235,8 @@ 'browser/extensions/api/identity/experimental_identity_api.h', 'browser/extensions/api/identity/experimental_web_auth_flow.cc', 'browser/extensions/api/identity/experimental_web_auth_flow.h', + 'browser/extensions/api/identity/gaia_web_auth_flow.cc', + 'browser/extensions/api/identity/gaia_web_auth_flow.h', 'browser/extensions/api/identity/identity_api.cc', 'browser/extensions/api/identity/identity_api.h', 'browser/extensions/api/identity/identity_mint_queue.cc', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index ccd468c..f068cb1 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -733,6 +733,7 @@ 'browser/extensions/api/extension_action/extension_browser_actions_api_unittest.cc', 'browser/extensions/api/file_system/file_system_api_unittest.cc', 'browser/extensions/api/identity/experimental_web_auth_flow_unittest.cc', + 'browser/extensions/api/identity/gaia_web_auth_flow_unittest.cc', 'browser/extensions/api/identity/identity_mint_queue_unittest.cc', 'browser/extensions/api/idle/idle_api_unittest.cc', 'browser/extensions/api/media_galleries_private/media_galleries_private_unittest.cc', diff --git a/google_apis/gaia/gaia_urls.cc b/google_apis/gaia/gaia_urls.cc index f5734c7..2135c31 100644 --- a/google_apis/gaia/gaia_urls.cc +++ b/google_apis/gaia/gaia_urls.cc @@ -31,6 +31,7 @@ const char kOAuthRevokeTokenUrlSuffix[] = "/AuthSubRevokeToken"; // API calls from accounts.google.com (LSO) const char kGetOAuthTokenUrlSuffix[] = "/o/oauth/GetOAuthToken/"; const char kClientLoginToOAuth2UrlSuffix[] = "/o/oauth2/programmatic_auth"; +const char kOAuth2AuthUrlSuffix[] = "/o/oauth2/auth"; const char kOAuth2RevokeUrlSuffix[] = "/o/oauth2/revoke"; const char kOAuth2TokenUrlSuffix[] = "/o/oauth2/token"; const char kClientOAuthUrlSuffix[] = "/ClientOAuth"; @@ -116,6 +117,7 @@ GaiaUrls::GaiaUrls() { get_oauth_token_url_ = lso_origin_url_ + kGetOAuthTokenUrlSuffix; std::string client_login_to_oauth2_url = lso_origin_url_ + kClientLoginToOAuth2UrlSuffix; + oauth2_auth_url_ = lso_origin_url_ + kOAuth2AuthUrlSuffix; std::string oauth2_token_url = lso_origin_url_ + kOAuth2TokenUrlSuffix; oauth2_revoke_url_ = lso_origin_url_ + kOAuth2RevokeUrlSuffix; @@ -229,6 +231,10 @@ const std::string& GaiaUrls::client_login_to_oauth2_url() { return client_login_to_oauth2_url_; } +const std::string& GaiaUrls::oauth2_auth_url() { + return oauth2_auth_url_; +} + const std::string& GaiaUrls::oauth2_token_url() { return oauth2_token_url_; } diff --git a/google_apis/gaia/gaia_urls.h b/google_apis/gaia/gaia_urls.h index 3fd19bc..3b434e4b 100644 --- a/google_apis/gaia/gaia_urls.h +++ b/google_apis/gaia/gaia_urls.h @@ -38,6 +38,7 @@ class GaiaUrls { 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_auth_url(); const std::string& oauth2_token_url(); const std::string& oauth2_issue_token_url(); const std::string& oauth2_revoke_url(); @@ -75,6 +76,7 @@ class GaiaUrls { std::string oauth2_chrome_client_id_; std::string oauth2_chrome_client_secret_; std::string client_login_to_oauth2_url_; + std::string oauth2_auth_url_; std::string oauth2_token_url_; std::string oauth2_issue_token_url_; std::string oauth2_revoke_url_; |