summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorcourage@chromium.org <courage@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-04-22 21:28:31 +0000
committercourage@chromium.org <courage@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-04-22 21:28:31 +0000
commitb796600e0558d4e374de157ae6db319786eff151 (patch)
treedefbc4420b45214e074ab4bd53814936c32d11ec
parentd5aba7e111cafa311a5d15ea63f42cbd835147f6 (diff)
downloadchromium_src-b796600e0558d4e374de157ae6db319786eff151.zip
chromium_src-b796600e0558d4e374de157ae6db319786eff151.tar.gz
chromium_src-b796600e0558d4e374de157ae6db319786eff151.tar.bz2
Identity API: Add token cache and identity.invalidateAuthToken.
Identity.getAuthToken caches access tokens from GAIA. Tokens expirations are handled automatically, but if an access token goes bad for some other reason, the application can call identity.invalidateAuthToken to remove a bad token from the cache. BUG=228908 (step #1 of the bug description) Review URL: https://chromiumcodereview.appspot.com/14329014 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@195604 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/extensions/api/identity/identity_api.cc218
-rw-r--r--chrome/browser/extensions/api/identity/identity_api.h65
-rw-r--r--chrome/browser/extensions/api/identity/identity_apitest.cc239
-rw-r--r--chrome/browser/extensions/extension_function_histogram_value.h1
-rw-r--r--chrome/common/extensions/api/experimental_identity.idl16
-rw-r--r--google_apis/gaia/oauth2_mint_token_flow.cc21
-rw-r--r--google_apis/gaia/oauth2_mint_token_flow.h8
-rw-r--r--google_apis/gaia/oauth2_mint_token_flow_unittest.cc17
-rw-r--r--tools/metrics/histograms/histograms.xml1
9 files changed, 535 insertions, 51 deletions
diff --git a/chrome/browser/extensions/api/identity/identity_api.cc b/chrome/browser/extensions/api/identity/identity_api.cc
index 479728c..30e1a42 100644
--- a/chrome/browser/extensions/api/identity/identity_api.cc
+++ b/chrome/browser/extensions/api/identity/identity_api.cc
@@ -48,6 +48,8 @@ const char kUserRejected[] = "The user did not approve access.";
const char kUserNotSignedIn[] = "The user is not signed in.";
const char kInteractionRequired[] = "User interaction required.";
const char kInvalidRedirect[] = "Did not redirect to the right URL.";
+
+const int kCachedIssueAdviceTTLSeconds = 1;
} // namespace identity_constants
namespace {
@@ -58,6 +60,8 @@ static const char kChromiumDomainRedirectUrlPattern[] =
} // namespace
namespace GetAuthToken = api::experimental_identity::GetAuthToken;
+namespace RemoveCachedAuthToken =
+ api::experimental_identity::RemoveCachedAuthToken;
namespace LaunchWebAuthFlow = api::experimental_identity::LaunchWebAuthFlow;
namespace identity = api::experimental_identity;
@@ -125,6 +129,10 @@ void IdentityGetAuthTokenFunction::CompleteFunctionWithError(
}
void IdentityGetAuthTokenFunction::StartSigninFlow() {
+ // All cached tokens are invalid because the user is not signed in.
+ IdentityAPI* id_api =
+ extensions::IdentityAPI::GetFactoryInstance()->GetForProfile(profile_);
+ id_api->EraseAllCachedTokens();
// Display a login prompt. If the subsequent mint fails, don't display the
// login prompt again.
should_prompt_for_signin_ = false;
@@ -143,14 +151,21 @@ void IdentityGetAuthTokenFunction::StartMintTokenFlow(
IdentityAPI* id_api =
extensions::IdentityAPI::GetFactoryInstance()->GetForProfile(profile_);
- // If there is an interactive flow in progress, non-interactive
- // requests should complete immediately since a consent UI is
- // known to be required.
- if (!should_prompt_for_scopes_ && !id_api->mint_queue()->empty(
- IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE,
- GetExtension()->id(), scopes)) {
- CompleteFunctionWithError(identity_constants::kNoGrant);
- return;
+ if (!should_prompt_for_scopes_) {
+ // Caller requested no interaction.
+
+ if (type == IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE) {
+ // GAIA told us to do a consent UI.
+ CompleteFunctionWithError(identity_constants::kNoGrant);
+ return;
+ }
+ if (!id_api->mint_queue()->empty(
+ IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE,
+ GetExtension()->id(), scopes)) {
+ // Another call is going through a consent UI.
+ CompleteFunctionWithError(identity_constants::kNoGrant);
+ return;
+ }
}
id_api->mint_queue()->RequestStart(type,
GetExtension()->id(),
@@ -174,24 +189,53 @@ void IdentityGetAuthTokenFunction::CompleteMintTokenFlow() {
void IdentityGetAuthTokenFunction::StartMintToken(
IdentityMintRequestQueue::MintType type) {
- switch (type) {
- case IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE:
- StartGaiaRequest(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
- break;
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension());
+ IdentityAPI* id_api = IdentityAPI::GetFactoryInstance()->GetForProfile(
+ profile());
+ IdentityTokenCacheValue cache_entry = id_api->GetCachedToken(
+ GetExtension()->id(), oauth2_info.scopes);
+ IdentityTokenCacheValue::CacheValueStatus cache_status =
+ cache_entry.status();
+
+ if (type == IdentityMintRequestQueue::MINT_TYPE_NONINTERACTIVE) {
+ switch (cache_status) {
+ case IdentityTokenCacheValue::CACHE_STATUS_NOTFOUND:
+ StartGaiaRequest(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
+ break;
+
+ case IdentityTokenCacheValue::CACHE_STATUS_TOKEN:
+ CompleteMintTokenFlow();
+ CompleteFunctionWithResult(cache_entry.token());
+ break;
+
+ case IdentityTokenCacheValue::CACHE_STATUS_ADVICE:
+ CompleteMintTokenFlow();
+ should_prompt_for_signin_ = false;
+ issue_advice_ = cache_entry.issue_advice();
+ StartMintTokenFlow(IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE);
+ break;
+ }
+ } else {
+ DCHECK(type == IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE);
- case IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE:
+ if (cache_status == IdentityTokenCacheValue::CACHE_STATUS_TOKEN) {
+ CompleteMintTokenFlow();
+ CompleteFunctionWithResult(cache_entry.token());
+ } else {
install_ui_.reset(new ExtensionInstallPrompt(GetAssociatedWebContents()));
ShowOAuthApprovalDialog(issue_advice_);
- break;
-
- default:
- NOTREACHED() << "Unexepected mint type in StartMintToken: " << type;
- break;
- };
+ }
+ }
}
void IdentityGetAuthTokenFunction::OnMintTokenSuccess(
- const std::string& access_token) {
+ const std::string& access_token, int time_to_live) {
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension());
+ IdentityTokenCacheValue token(access_token,
+ base::TimeDelta::FromSeconds(time_to_live));
+ IdentityAPI::GetFactoryInstance()->GetForProfile(profile())->SetCachedToken(
+ GetExtension()->id(), oauth2_info.scopes, token);
+
CompleteMintTokenFlow();
CompleteFunctionWithResult(access_token);
}
@@ -223,17 +267,17 @@ void IdentityGetAuthTokenFunction::OnMintTokenFailure(
void IdentityGetAuthTokenFunction::OnIssueAdviceSuccess(
const IssueAdviceInfo& issue_advice) {
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(GetExtension());
+ IdentityAPI::GetFactoryInstance()->GetForProfile(profile())->SetCachedToken(
+ GetExtension()->id(), oauth2_info.scopes,
+ IdentityTokenCacheValue(issue_advice));
CompleteMintTokenFlow();
should_prompt_for_signin_ = false;
// Existing grant was revoked and we used NO_FORCE, so we got info back
- // instead.
- if (should_prompt_for_scopes_) {
- issue_advice_ = issue_advice;
- StartMintTokenFlow(IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE);
- } else {
- CompleteFunctionWithError(identity_constants::kNoGrant);
- }
+ // instead. Start a consent UI if we can.
+ issue_advice_ = issue_advice;
+ StartMintTokenFlow(IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE);
}
void IdentityGetAuthTokenFunction::SigninSuccess(const std::string& token) {
@@ -305,6 +349,23 @@ bool IdentityGetAuthTokenFunction::HasLoginToken() const {
return token_service->HasOAuthLoginToken();
}
+IdentityRemoveCachedAuthTokenFunction::IdentityRemoveCachedAuthTokenFunction() {
+}
+
+IdentityRemoveCachedAuthTokenFunction::
+ ~IdentityRemoveCachedAuthTokenFunction() {
+}
+
+bool IdentityRemoveCachedAuthTokenFunction::RunImpl() {
+ scoped_ptr<RemoveCachedAuthToken::Params> params(
+ RemoveCachedAuthToken::Params::Create(*args_));
+ EXTENSION_FUNCTION_VALIDATE(params.get());
+ const identity::InvalidTokenDetails& details = params->details;
+ IdentityAPI::GetFactoryInstance()->GetForProfile(profile())->EraseCachedToken(
+ GetExtension()->id(), details.token);
+ return true;
+}
+
IdentityLaunchWebAuthFlowFunction::IdentityLaunchWebAuthFlowFunction() {}
IdentityLaunchWebAuthFlowFunction::~IdentityLaunchWebAuthFlowFunction() {}
@@ -397,6 +458,52 @@ void IdentityLaunchWebAuthFlowFunction::OnAuthFlowURLChange(
}
}
+IdentityTokenCacheValue::IdentityTokenCacheValue()
+ : status_(CACHE_STATUS_NOTFOUND) {
+}
+
+IdentityTokenCacheValue::IdentityTokenCacheValue(
+ const IssueAdviceInfo& issue_advice) : status_(CACHE_STATUS_ADVICE),
+ issue_advice_(issue_advice) {
+ expiration_time_ = base::Time::Now() + base::TimeDelta::FromSeconds(
+ identity_constants::kCachedIssueAdviceTTLSeconds);
+}
+
+IdentityTokenCacheValue::IdentityTokenCacheValue(
+ const std::string& token, base::TimeDelta time_to_live)
+ : status_(CACHE_STATUS_TOKEN),
+ token_(token) {
+ base::TimeDelta zero_delta;
+ if (time_to_live < zero_delta)
+ time_to_live = zero_delta;
+
+ expiration_time_ = base::Time::Now() + time_to_live;
+}
+
+IdentityTokenCacheValue::~IdentityTokenCacheValue() {
+}
+
+IdentityTokenCacheValue::CacheValueStatus
+ IdentityTokenCacheValue::status() const {
+ if (is_expired())
+ return IdentityTokenCacheValue::CACHE_STATUS_NOTFOUND;
+ else
+ return status_;
+}
+
+const IssueAdviceInfo& IdentityTokenCacheValue::issue_advice() const {
+ return issue_advice_;
+}
+
+const std::string& IdentityTokenCacheValue::token() const {
+ return token_;
+}
+
+bool IdentityTokenCacheValue::is_expired() const {
+ return status_ == CACHE_STATUS_NOTFOUND ||
+ expiration_time_ < base::Time::Now();
+}
+
IdentityAPI::IdentityAPI(Profile* profile)
: profile_(profile),
signin_manager_(NULL),
@@ -421,6 +528,44 @@ IdentityMintRequestQueue* IdentityAPI::mint_queue() {
return &mint_queue_;
}
+void IdentityAPI::SetCachedToken(const std::string& extension_id,
+ const std::vector<std::string> scopes,
+ const IdentityTokenCacheValue& token_data) {
+ std::set<std::string> scopeset(scopes.begin(), scopes.end());
+ TokenCacheKey key(extension_id, scopeset);
+
+ std::map<TokenCacheKey, IdentityTokenCacheValue>::iterator it =
+ token_cache_.find(key);
+ if (it != token_cache_.end() && it->second.status() <= token_data.status())
+ token_cache_.erase(it);
+
+ token_cache_.insert(std::make_pair(key, token_data));
+}
+
+void IdentityAPI::EraseCachedToken(const std::string& extension_id,
+ const std::string& token) {
+ std::map<TokenCacheKey, IdentityTokenCacheValue>::iterator it;
+ for (it = token_cache_.begin(); it != token_cache_.end(); ++it) {
+ if (it->first.extension_id == extension_id &&
+ it->second.status() == IdentityTokenCacheValue::CACHE_STATUS_TOKEN &&
+ it->second.token() == token) {
+ token_cache_.erase(it);
+ break;
+ }
+ }
+}
+
+void IdentityAPI::EraseAllCachedTokens() {
+ token_cache_.clear();
+}
+
+const IdentityTokenCacheValue& IdentityAPI::GetCachedToken(
+ const std::string& extension_id, const std::vector<std::string> scopes) {
+ std::set<std::string> scopeset(scopes.begin(), scopes.end());
+ TokenCacheKey key(extension_id, scopeset);
+ return token_cache_[key];
+}
+
void IdentityAPI::ReportAuthError(const GoogleServiceAuthError& error) {
if (!signin_manager_)
Initialize();
@@ -466,4 +611,23 @@ void ProfileKeyedAPIFactory<IdentityAPI>::DeclareFactoryDependencies() {
DependsOn(SigninManagerFactory::GetInstance());
}
+IdentityAPI::TokenCacheKey::TokenCacheKey(const std::string& extension_id,
+ const std::set<std::string> scopes)
+ : extension_id(extension_id),
+ scopes(scopes) {
+}
+
+IdentityAPI::TokenCacheKey::~TokenCacheKey() {
+}
+
+bool IdentityAPI::TokenCacheKey::operator<(
+ const IdentityAPI::TokenCacheKey& rhs) const {
+ if (extension_id < rhs.extension_id)
+ return true;
+ else if (rhs.extension_id < extension_id)
+ return false;
+
+ return scopes < rhs.scopes;
+}
+
} // namespace extensions
diff --git a/chrome/browser/extensions/api/identity/identity_api.h b/chrome/browser/extensions/api/identity/identity_api.h
index f7b4448..f8a164e 100644
--- a/chrome/browser/extensions/api/identity/identity_api.h
+++ b/chrome/browser/extensions/api/identity/identity_api.h
@@ -90,7 +90,8 @@ class IdentityGetAuthTokenFunction : public AsyncExtensionFunction,
virtual void StartMintToken(IdentityMintRequestQueue::MintType type) OVERRIDE;
// OAuth2MintTokenFlow::Delegate implementation:
- virtual void OnMintTokenSuccess(const std::string& access_token) OVERRIDE;
+ virtual void OnMintTokenSuccess(const std::string& access_token,
+ int time_to_live) OVERRIDE;
virtual void OnMintTokenFailure(
const GoogleServiceAuthError& error) OVERRIDE;
virtual void OnIssueAdviceSuccess(
@@ -130,6 +131,19 @@ class IdentityGetAuthTokenFunction : public AsyncExtensionFunction,
scoped_ptr<IdentitySigninFlow> signin_flow_;
};
+class IdentityRemoveCachedAuthTokenFunction : public SyncExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("experimental.identity.removeCachedAuthToken",
+ EXPERIMENTAL_IDENTITY_REMOVECACHEDAUTHTOKEN)
+ IdentityRemoveCachedAuthTokenFunction();
+
+ protected:
+ virtual ~IdentityRemoveCachedAuthTokenFunction();
+
+ // SyncExtensionFunction implementation:
+ virtual bool RunImpl() OVERRIDE;
+};
+
class IdentityLaunchWebAuthFlowFunction : public AsyncExtensionFunction,
public WebAuthFlow::Delegate {
public:
@@ -160,6 +174,35 @@ class IdentityLaunchWebAuthFlowFunction : public AsyncExtensionFunction,
std::vector<GURL> final_prefixes_;
};
+class IdentityTokenCacheValue {
+ public:
+ IdentityTokenCacheValue();
+ explicit IdentityTokenCacheValue(const IssueAdviceInfo& issue_advice);
+ IdentityTokenCacheValue(const std::string& token,
+ base::TimeDelta time_to_live);
+ ~IdentityTokenCacheValue();
+
+ // Order of these entries is used to determine whether or not new
+ // entries supercede older ones in SetCachedToken.
+ enum CacheValueStatus {
+ CACHE_STATUS_NOTFOUND,
+ CACHE_STATUS_ADVICE,
+ CACHE_STATUS_TOKEN
+ };
+
+ CacheValueStatus status() const;
+ const IssueAdviceInfo& issue_advice() const;
+ const std::string& token() const;
+
+ private:
+ bool is_expired() const;
+
+ CacheValueStatus status_;
+ IssueAdviceInfo issue_advice_;
+ std::string token_;
+ base::Time expiration_time_;
+};
+
class IdentityAPI : public ProfileKeyedAPI,
public SigninGlobalError::AuthStatusProvider,
public content::NotificationObserver {
@@ -171,6 +214,16 @@ class IdentityAPI : public ProfileKeyedAPI,
// Request serialization queue for getAuthToken.
IdentityMintRequestQueue* mint_queue();
+ // Token cache
+ void SetCachedToken(const std::string& extension_id,
+ const std::vector<std::string> scopes,
+ const IdentityTokenCacheValue& token_data);
+ void EraseCachedToken(const std::string& extension_id,
+ const std::string& token);
+ void EraseAllCachedTokens();
+ const IdentityTokenCacheValue& GetCachedToken(
+ const std::string& extension_id, const std::vector<std::string> scopes);
+
void ReportAuthError(const GoogleServiceAuthError& error);
// ProfileKeyedAPI implementation.
@@ -188,6 +241,15 @@ class IdentityAPI : public ProfileKeyedAPI,
private:
friend class ProfileKeyedAPIFactory<IdentityAPI>;
+ struct TokenCacheKey {
+ TokenCacheKey(const std::string& extension_id,
+ const std::set<std::string> scopes);
+ ~TokenCacheKey();
+ bool operator<(const TokenCacheKey& rhs) const;
+ std::string extension_id;
+ std::set<std::string> scopes;
+ };
+
// ProfileKeyedAPI implementation.
static const char* service_name() {
return "IdentityAPI";
@@ -200,6 +262,7 @@ class IdentityAPI : public ProfileKeyedAPI,
// Used to listen to notifications from the TokenService.
content::NotificationRegistrar registrar_;
IdentityMintRequestQueue mint_queue_;
+ std::map<TokenCacheKey, IdentityTokenCacheValue> token_cache_;
};
template <>
diff --git a/chrome/browser/extensions/api/identity/identity_apitest.cc b/chrome/browser/extensions/api/identity/identity_apitest.cc
index 9bf130d..0b04145 100644
--- a/chrome/browser/extensions/api/identity/identity_apitest.cc
+++ b/chrome/browser/extensions/api/identity/identity_apitest.cc
@@ -19,6 +19,7 @@
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "content/public/test/test_utils.h"
+#include "extensions/common/id_util.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "google_apis/gaia/oauth2_mint_token_flow.h"
#include "googleurl/src/gurl.h"
@@ -37,6 +38,7 @@ namespace errors = identity_constants;
namespace utils = extension_function_test_utils;
static const char kAccessToken[] = "auth_token";
+static const char kExtensionId[] = "ext_id";
// This helps us be able to wait until an AsyncExtensionFunction calls
// SendResponse.
@@ -158,7 +160,7 @@ class TestOAuth2MintTokenFlow : public OAuth2MintTokenFlow {
break;
}
case MINT_TOKEN_SUCCESS: {
- delegate_->OnMintTokenSuccess(kAccessToken);
+ delegate_->OnMintTokenSuccess(kAccessToken, 3600);
break;
}
case MINT_TOKEN_FAILURE: {
@@ -273,9 +275,9 @@ class GetAuthTokenFunctionTest : public AsyncExtensionBrowserTest {
return ext;
}
- void InitializeTestAPIFactory() {
- IdentityAPI::GetFactoryInstance()->SetTestingFactory(
- browser()->profile(), &IdentityAPITestFactory);
+ IdentityAPI* id_api() {
+ return IdentityAPI::GetFactoryInstance()->GetForProfile(
+ browser()->profile());
}
};
@@ -330,6 +332,28 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
}
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
+ NonInteractiveMintAdviceSuccess) {
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ 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));
+ std::string error = utils::RunFunctionAndReturnError(
+ func.get(), "[{}]", browser());
+ EXPECT_TRUE(StartsWithASCII(error, errors::kNoGrant, false));
+ EXPECT_FALSE(func->login_ui_shown());
+ EXPECT_FALSE(func->install_ui_shown());
+
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
+ EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_ADVICE,
+ id_api()->GetCachedToken(extension->id(),
+ oauth2_info.scopes).status());
+}
+
+IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
NonInteractiveMintBadCredentials) {
scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
func->set_extension(CreateExtension(CLIENT_ID | SCOPES));
@@ -348,7 +372,9 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
NonInteractiveSuccess) {
scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
- func->set_extension(CreateExtension(CLIENT_ID | SCOPES));
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ func->set_extension(extension);
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
EXPECT_CALL(*func.get(), HasLoginToken())
.WillOnce(Return(true));
TestOAuth2MintTokenFlow* flow = new TestOAuth2MintTokenFlow(
@@ -361,6 +387,9 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
EXPECT_EQ(std::string(kAccessToken), access_token);
EXPECT_FALSE(func->login_ui_shown());
EXPECT_FALSE(func->install_ui_shown());
+ EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_TOKEN,
+ id_api()->GetCachedToken(extension->id(),
+ oauth2_info.scopes).status());
}
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
@@ -486,7 +515,9 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
InteractiveLoginSuccessApprovalDoneMintSuccess) {
scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
- func->set_extension(CreateExtension(CLIENT_ID | SCOPES));
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ 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);
@@ -506,6 +537,9 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
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());
}
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
@@ -572,7 +606,6 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
}
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, NoninteractiveQueue) {
- InitializeTestAPIFactory();
scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
func->set_extension(extension);
@@ -615,7 +648,6 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, NoninteractiveQueue) {
}
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, InteractiveQueue) {
- InitializeTestAPIFactory();
scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
func->set_extension(extension);
@@ -665,7 +697,6 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest, InteractiveQueue) {
IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
InteractiveQueuedNoninteractiveFails) {
- InitializeTestAPIFactory();
scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
func->set_extension(extension);
@@ -697,6 +728,196 @@ IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
queue->RequestComplete(type, extension->id(), scopes, &queued_request);
}
+IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
+ NonInteractiveCacheHit) {
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
+ func->set_extension(extension);
+
+ // pre-populate the cache with a token
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
+ IdentityTokenCacheValue token(kAccessToken,
+ base::TimeDelta::FromSeconds(3600));
+ id_api()->SetCachedToken(extension->id(), oauth2_info.scopes, token);
+
+ // Get a token. Should not require a GAIA request.
+ EXPECT_CALL(*func.get(), HasLoginToken())
+ .WillOnce(Return(true));
+ scoped_ptr<base::Value> value(utils::RunFunctionAndReturnSingleResult(
+ func.get(), "[{}]", 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_FALSE(func->install_ui_shown());
+}
+
+IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
+ NonInteractiveIssueAdviceCacheHit) {
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
+ func->set_extension(extension);
+
+ // pre-populate the cache with advice
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
+ IssueAdviceInfo info;
+ IdentityTokenCacheValue token(info);
+ id_api()->SetCachedToken(extension->id(), oauth2_info.scopes, token);
+
+ // Should return an error without a GAIA request.
+ EXPECT_CALL(*func.get(), HasLoginToken())
+ .WillOnce(Return(true));
+ std::string error = utils::RunFunctionAndReturnError(
+ func.get(), "[{}]", browser());
+ EXPECT_EQ(std::string(errors::kNoGrant), error);
+ EXPECT_FALSE(func->login_ui_shown());
+ EXPECT_FALSE(func->install_ui_shown());
+}
+
+IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
+ InteractiveCacheHit) {
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
+ func->set_extension(extension);
+
+ // Create a fake request to block the queue.
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
+ std::set<std::string> scopes(oauth2_info.scopes.begin(),
+ oauth2_info.scopes.end());
+ IdentityMintRequestQueue* queue = id_api()->mint_queue();
+ MockQueuedMintRequest queued_request;
+ IdentityMintRequestQueue::MintType type =
+ IdentityMintRequestQueue::MINT_TYPE_INTERACTIVE;
+
+ EXPECT_CALL(queued_request, StartMintToken(type)).Times(1);
+ queue->RequestStart(type, extension->id(), scopes, &queued_request);
+
+ // The real request will start processing, but wait in the queue behind
+ // the blocker.
+ 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));
+ RunFunctionAsync(func, "[{\"interactive\": true}]");
+
+ // Populate the cache with a token while the request is blocked.
+ IdentityTokenCacheValue token(kAccessToken,
+ base::TimeDelta::FromSeconds(3600));
+ id_api()->SetCachedToken(extension->id(), oauth2_info.scopes, token);
+
+ // When we wake up the request, it returns the cached token without
+ // displaying a UI, or hitting GAIA.
+
+ queue->RequestComplete(type, extension->id(), scopes, &queued_request);
+
+ scoped_ptr<base::Value> value(WaitForSingleResult(func));
+ std::string access_token;
+ 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());
+}
+
+IN_PROC_BROWSER_TEST_F(GetAuthTokenFunctionTest,
+ LoginInvalidatesTokenCache) {
+ scoped_refptr<MockGetAuthTokenFunction> func(new MockGetAuthTokenFunction());
+ scoped_refptr<const Extension> extension(CreateExtension(CLIENT_ID | SCOPES));
+ func->set_extension(extension);
+
+ // pre-populate the cache with a token
+ const OAuth2Info& oauth2_info = OAuth2Info::GetOAuth2Info(extension);
+ IdentityTokenCacheValue token(kAccessToken,
+ base::TimeDelta::FromSeconds(3600));
+ id_api()->SetCachedToken(extension->id(), oauth2_info.scopes, token);
+
+ // Because the user is not signed in, the token will be removed,
+ // and we'll hit GAIA for new tokens.
+ 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_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_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());
+}
+
+class RemoveCachedAuthTokenFunctionTest : public ExtensionBrowserTest {
+ protected:
+ bool InvalidateDefaultToken() {
+ scoped_refptr<IdentityRemoveCachedAuthTokenFunction> func(
+ new IdentityRemoveCachedAuthTokenFunction);
+ func->set_extension(utils::CreateEmptyExtension(kExtensionId));
+ return utils::RunFunction(
+ func, std::string("[{\"token\": \"") + kAccessToken + "\"}]", browser(),
+ extension_function_test_utils::NONE);
+ }
+
+ IdentityAPI* id_api() {
+ return IdentityAPI::GetFactoryInstance()->GetForProfile(
+ browser()->profile());
+ }
+
+ void SetCachedToken(IdentityTokenCacheValue& token_data) {
+ id_api()->SetCachedToken(extensions::id_util::GenerateId(kExtensionId),
+ std::vector<std::string>(), token_data);
+ }
+
+ const IdentityTokenCacheValue& GetCachedToken() {
+ return id_api()->GetCachedToken(
+ extensions::id_util::GenerateId(kExtensionId),
+ std::vector<std::string>());
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(RemoveCachedAuthTokenFunctionTest, NotFound) {
+ EXPECT_TRUE(InvalidateDefaultToken());
+ EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_NOTFOUND,
+ GetCachedToken().status());
+}
+
+IN_PROC_BROWSER_TEST_F(RemoveCachedAuthTokenFunctionTest, Advice) {
+ IssueAdviceInfo info;
+ IdentityTokenCacheValue advice(info);
+ SetCachedToken(advice);
+ EXPECT_TRUE(InvalidateDefaultToken());
+ EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_ADVICE,
+ GetCachedToken().status());
+}
+
+IN_PROC_BROWSER_TEST_F(RemoveCachedAuthTokenFunctionTest, NonMatchingToken) {
+ IdentityTokenCacheValue token("non_matching_token",
+ base::TimeDelta::FromSeconds(3600));
+ SetCachedToken(token);
+ EXPECT_TRUE(InvalidateDefaultToken());
+ EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_TOKEN,
+ GetCachedToken().status());
+ EXPECT_EQ("non_matching_token", GetCachedToken().token());
+}
+
+IN_PROC_BROWSER_TEST_F(RemoveCachedAuthTokenFunctionTest, MatchingToken) {
+ IdentityTokenCacheValue token(kAccessToken,
+ base::TimeDelta::FromSeconds(3600));
+ SetCachedToken(token);
+ EXPECT_TRUE(InvalidateDefaultToken());
+ EXPECT_EQ(IdentityTokenCacheValue::CACHE_STATUS_NOTFOUND,
+ GetCachedToken().status());
+}
+
class LaunchWebAuthFlowFunctionTest : public AsyncExtensionBrowserTest {
protected:
void RunAndCheckBounds(
diff --git a/chrome/browser/extensions/extension_function_histogram_value.h b/chrome/browser/extensions/extension_function_histogram_value.h
index c74cabc5..150c4f5 100644
--- a/chrome/browser/extensions/extension_function_histogram_value.h
+++ b/chrome/browser/extensions/extension_function_histogram_value.h
@@ -515,6 +515,7 @@ enum HistogramValue {
BLUETOOTH_ADDPROFILE,
BLUETOOTH_REMOVEPROFILE,
BLUETOOTH_GETPROFILES,
+ EXPERIMENTAL_IDENTITY_REMOVECACHEDAUTHTOKEN,
ENUM_BOUNDARY // Last entry: Add new entries above.
};
diff --git a/chrome/common/extensions/api/experimental_identity.idl b/chrome/common/extensions/api/experimental_identity.idl
index 625b0cd..247af45 100644
--- a/chrome/common/extensions/api/experimental_identity.idl
+++ b/chrome/common/extensions/api/experimental_identity.idl
@@ -10,6 +10,11 @@ namespace experimental.identity {
boolean? interactive;
};
+ [inline_doc] dictionary InvalidTokenDetails {
+ // The specific token that should be removed from the cache.
+ DOMString token;
+ };
+
[inline_doc] dictionary WebAuthFlowDetails {
// The URL that initiates the auth flow.
DOMString url;
@@ -31,6 +36,7 @@ namespace experimental.identity {
};
callback GetAuthTokenCallback = void (optional DOMString token);
+ callback InvalidateAuthTokenCallback = void ();
callback LaunchWebAuthFlowCallback = void (optional DOMString responseUrl);
interface Functions {
@@ -42,6 +48,16 @@ namespace experimental.identity {
static void getAuthToken(optional TokenDetails details,
GetAuthTokenCallback callback);
+ // Removes an OAuth2 access token from the Identity API's token cache.
+ // When an access token is discovered to be invalid, it should be
+ // passed to removeCachedAuthToken to remove it from the cache. The
+ // app may then retrieve a fresh token with getAuthToken.
+ //
+ // |details| : Token information.
+ // |callback| : Called when the token has been removed from the cache.
+ static void removeCachedAuthToken(InvalidTokenDetails details,
+ InvalidateAuthTokenCallback callback);
+
// Starts an auth flow at the specified URL.
//
// |details| : WebAuth flow options.
diff --git a/google_apis/gaia/oauth2_mint_token_flow.cc b/google_apis/gaia/oauth2_mint_token_flow.cc
index 169d187..9c5d943 100644
--- a/google_apis/gaia/oauth2_mint_token_flow.cc
+++ b/google_apis/gaia/oauth2_mint_token_flow.cc
@@ -14,6 +14,7 @@
#include "base/message_loop.h"
#include "base/string_util.h"
#include "base/stringprintf.h"
+#include "base/strings/string_number_conversions.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "google_apis/gaia/gaia_urls.h"
@@ -45,6 +46,7 @@ static const char kIssueAdviceValueAuto[] = "auto";
static const char kIssueAdviceValueConsent[] = "consent";
static const char kAccessTokenKey[] = "token";
static const char kConsentKey[] = "consent";
+static const char kExpiresInKey[] = "expiresIn";
static const char kScopesKey[] = "scopes";
static const char kDescriptionKey[] = "description";
static const char kDetailKey[] = "detail";
@@ -102,9 +104,10 @@ OAuth2MintTokenFlow::OAuth2MintTokenFlow(URLRequestContextGetter* context,
OAuth2MintTokenFlow::~OAuth2MintTokenFlow() { }
-void OAuth2MintTokenFlow::ReportSuccess(const std::string& access_token) {
+void OAuth2MintTokenFlow::ReportSuccess(const std::string& access_token,
+ int time_to_live) {
if (delegate_)
- delegate_->OnMintTokenSuccess(access_token);
+ delegate_->OnMintTokenSuccess(access_token, time_to_live);
// |this| may already be deleted.
}
@@ -174,8 +177,9 @@ void OAuth2MintTokenFlow::ProcessApiCallSuccess(
ReportFailure(GoogleServiceAuthError::FromConnectionError(101));
} else {
std::string access_token;
- if (ParseMintTokenResponse(dict, &access_token))
- ReportSuccess(access_token);
+ int time_to_live;
+ if (ParseMintTokenResponse(dict, &access_token, &time_to_live))
+ ReportSuccess(access_token, time_to_live);
else
ReportFailure(GoogleServiceAuthError::FromConnectionError(101));
}
@@ -200,10 +204,15 @@ void OAuth2MintTokenFlow::ProcessMintAccessTokenFailure(
// static
bool OAuth2MintTokenFlow::ParseMintTokenResponse(
- const base::DictionaryValue* dict, std::string* access_token) {
+ const base::DictionaryValue* dict, std::string* access_token,
+ int* time_to_live) {
CHECK(dict);
CHECK(access_token);
- return dict->GetString(kAccessTokenKey, access_token);
+ CHECK(time_to_live);
+ std::string ttl_string;
+ return dict->GetString(kExpiresInKey, &ttl_string) &&
+ base::StringToInt(ttl_string, time_to_live) &&
+ dict->GetString(kAccessTokenKey, access_token);
}
// static
diff --git a/google_apis/gaia/oauth2_mint_token_flow.h b/google_apis/gaia/oauth2_mint_token_flow.h
index 40850b4..e2824e6 100644
--- a/google_apis/gaia/oauth2_mint_token_flow.h
+++ b/google_apis/gaia/oauth2_mint_token_flow.h
@@ -90,7 +90,8 @@ class OAuth2MintTokenFlow : public OAuth2ApiCallFlow {
class Delegate {
public:
- virtual void OnMintTokenSuccess(const std::string& access_token) {}
+ virtual void OnMintTokenSuccess(const std::string& access_token,
+ int time_to_live) {}
virtual void OnIssueAdviceSuccess(const IssueAdviceInfo& issue_advice) {}
virtual void OnMintTokenFailure(const GoogleServiceAuthError& error) {}
@@ -126,14 +127,15 @@ class OAuth2MintTokenFlow : public OAuth2ApiCallFlow {
FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest,
ProcessMintAccessTokenFailure);
- void ReportSuccess(const std::string& access_token);
+ void ReportSuccess(const std::string& access_token, int time_to_live);
void ReportIssueAdviceSuccess(const IssueAdviceInfo& issue_advice);
void ReportFailure(const GoogleServiceAuthError& error);
static bool ParseIssueAdviceResponse(
const base::DictionaryValue* dict, IssueAdviceInfo* issue_advice);
static bool ParseMintTokenResponse(
- const base::DictionaryValue* dict, std::string* access_token);
+ const base::DictionaryValue* dict, std::string* access_token,
+ int* time_to_live);
Delegate* delegate_;
Parameters parameters_;
diff --git a/google_apis/gaia/oauth2_mint_token_flow_unittest.cc b/google_apis/gaia/oauth2_mint_token_flow_unittest.cc
index 3206601..48a9ff1 100644
--- a/google_apis/gaia/oauth2_mint_token_flow_unittest.cc
+++ b/google_apis/gaia/oauth2_mint_token_flow_unittest.cc
@@ -29,7 +29,8 @@ namespace {
static const char kValidTokenResponse[] =
"{"
" \"token\": \"at1\","
- " \"issueAdvice\": \"Auto\""
+ " \"issueAdvice\": \"Auto\","
+ " \"expiresIn\": \"3600\""
"}";
static const char kTokenResponseNoAccessToken[] =
"{"
@@ -126,7 +127,8 @@ class MockDelegate : public OAuth2MintTokenFlow::Delegate {
MockDelegate() {}
~MockDelegate() {}
- MOCK_METHOD1(OnMintTokenSuccess, void(const std::string& access_token));
+ MOCK_METHOD2(OnMintTokenSuccess, void(const std::string& access_token,
+ int time_to_live));
MOCK_METHOD1(OnIssueAdviceSuccess,
void (const IssueAdviceInfo& issue_advice));
MOCK_METHOD1(OnMintTokenFailure,
@@ -230,14 +232,19 @@ TEST_F(OAuth2MintTokenFlowTest, ParseMintTokenResponse) {
scoped_ptr<base::DictionaryValue> json(
ParseJson(kTokenResponseNoAccessToken));
std::string at;
- EXPECT_FALSE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at));
+ int ttl;
+ EXPECT_FALSE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at,
+ &ttl));
EXPECT_TRUE(at.empty());
}
{ // All good.
scoped_ptr<base::DictionaryValue> json(ParseJson(kValidTokenResponse));
std::string at;
- EXPECT_TRUE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at));
+ int ttl;
+ EXPECT_TRUE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at,
+ &ttl));
EXPECT_EQ("at1", at);
+ EXPECT_EQ(3600, ttl);
}
}
@@ -295,7 +302,7 @@ TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallSuccess) {
TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL);
url_fetcher.SetResponseString(kValidTokenResponse);
CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE);
- EXPECT_CALL(delegate_, OnMintTokenSuccess("at1"));
+ EXPECT_CALL(delegate_, OnMintTokenSuccess("at1", 3600));
flow_->ProcessApiCallSuccess(&url_fetcher);
}
{ // Valid json: no description.
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index cadf946..84a7285 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -4975,6 +4975,7 @@ other types of suffix sets.
<int value="436" label="SYNCFILESYSTEM_GETFILESYNCSTATUSES"/>
<int value="437" label="MEDIAGALLERIESPRIVATE_GETHANDLERS"/>
<int value="438" label="WALLPAPERPRIVATE_RESETWALLPAPER"/>
+ <int value="439" label="EXPERIMENTAL_IDENTITY_REMOVECACHEDAUTHTOKEN"/>
</enum>
<enum name="FallbackSSLVersion" type="int">