From 524faceb97b4b2e3fd4cde0e646e50e58e752704 Mon Sep 17 00:00:00 2001 From: Michael Ochmann Date: Tue, 15 Dec 2015 15:40:12 +0100 Subject: [PATCH] Cache for OAuth access tokens OAuth access tokens retrieved during login in the web UI are stored privately in OAuthSession. There is no possibility for a user to obtain that token, e.g. to authenticate with a native Git client. This patch adds a persistent cache for OAuth tokens and modifies OAuthSession to store tokens received during the login handshake with the OAuth provider in this cache. Since access tokens must be kept secret, the cache defines a new extension point OAuthTokenEncrypter. If an encrypter is provided, access tokens are encrypted before storing them in the cache, and decrypted when reading from the cache. By default, no encryption is applied. In subsequent patches a REST API for retrieving OAuth tokens will be added as well as a corresponding settings page. Change-Id: I751dd5f70dd30823bd2f531e1ac1da0759f98976 Signed-off-by: Michael Ochmann --- .../extensions/auth/oauth/OAuthToken.java | 26 ++++- .../auth/oauth/OAuthTokenEncrypter.java | 35 ++++++ .../gerrit/httpd/auth/oauth/OAuthSession.java | 21 ++-- .../server/auth/oauth/OAuthTokenCache.java | 105 ++++++++++++++++++ .../server/config/GerritGlobalModule.java | 4 + 5 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java index 901951ed67..99b2cfa8ea 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthToken.java @@ -14,17 +14,33 @@ package com.google.gerrit.extensions.auth.oauth; +import java.io.Serializable; + /* OAuth token */ -public class OAuthToken { +public class OAuthToken implements Serializable { + + private static final long serialVersionUID = 1L; private final String token; private final String secret; private final String raw; + /** + * Time of expiration of this token, or {@code Long#MAX_VALUE} if this + * token never expires, or time of expiration is unknown. + */ + private final long expiresAt; + public OAuthToken(String token, String secret, String raw) { + this(token, secret, raw, Long.MAX_VALUE); + } + + public OAuthToken(String token, String secret, String raw, + long expiresAt) { this.token = token; this.secret = secret; this.raw = raw; + this.expiresAt = expiresAt; } public String getToken() { @@ -38,4 +54,12 @@ public class OAuthToken { public String getRaw() { return raw; } + + public long getExpiresAt() { + return expiresAt; + } + + public boolean isExpired() { + return System.currentTimeMillis() > expiresAt; + } } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java new file mode 100644 index 0000000000..b2f426234d --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthTokenEncrypter.java @@ -0,0 +1,35 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.extensions.auth.oauth; + +import com.google.gerrit.extensions.annotations.ExtensionPoint; + +@ExtensionPoint +public interface OAuthTokenEncrypter { + + /** + * Encrypts the secret parts of the given OAuth access token. + * + * @param unencrypted a raw OAuth access token. + */ + OAuthToken encrypt(OAuthToken unencrypted); + + /** + * Decrypts the secret parts of the given OAuth access token. + * + * @param encrypted an encryppted OAuth access token. + */ + OAuthToken decrypt(OAuthToken encrypted); +} diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java index d24c8a0017..3e659127c5 100644 --- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java +++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java @@ -31,6 +31,7 @@ import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthResult; +import com.google.gerrit.server.auth.oauth.OAuthTokenCache; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -58,8 +59,8 @@ class OAuthSession { private final Provider identifiedUser; private final AccountManager accountManager; private final CanonicalWebUrl urlProvider; + private final OAuthTokenCache tokenCache; private OAuthServiceProvider serviceProvider; - private OAuthToken token; private OAuthUserInfo user; private String redirectToken; private boolean linkMode; @@ -68,16 +69,18 @@ class OAuthSession { OAuthSession(DynamicItem webSession, Provider identifiedUser, AccountManager accountManager, - CanonicalWebUrl urlProvider) { + CanonicalWebUrl urlProvider, + OAuthTokenCache tokenCache) { this.state = generateRandomState(); this.identifiedUser = identifiedUser; this.webSession = webSession; this.accountManager = accountManager; this.urlProvider = urlProvider; + this.tokenCache = tokenCache; } boolean isLoggedIn() { - return token != null && user != null; + return tokenCache.has(user); } boolean isOAuthFinal(HttpServletRequest request) { @@ -95,9 +98,12 @@ class OAuthSession { } log.debug("Login-Retrieve-User " + this); - token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code"))); - + OAuthToken token = oauth.getAccessToken( + new OAuthVerifier(request.getParameter("code"))); user = oauth.getUserInfo(token); + if (user != null && token != null) { + tokenCache.put(user, token); + } if (isLoggedIn()) { log.debug("Login-SUCCESS " + this); @@ -211,7 +217,7 @@ class OAuthSession { } void logout() { - token = null; + tokenCache.remove(user); user = null; redirectToken = null; serviceProvider = null; @@ -243,7 +249,8 @@ class OAuthSession { @Override public String toString() { - return "OAuthSession [token=" + token + ", user=" + user + "]"; + return "OAuthSession [token=" + tokenCache.get(user) + ", user=" + user + + "]"; } public void setServiceProvider(OAuthServiceProvider provider) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java new file mode 100644 index 0000000000..bcc5eab043 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java @@ -0,0 +1,105 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.auth.oauth; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.cache.Cache; +import com.google.gerrit.extensions.auth.oauth.OAuthToken; +import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter; +import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.server.cache.CacheModule; +import com.google.inject.Inject; +import com.google.inject.Module; +import com.google.inject.Singleton; +import com.google.inject.name.Named; + +@Singleton +public class OAuthTokenCache { + public static final String OAUTH_TOKENS = "oauth_tokens"; + + private final DynamicItem encrypter; + + public static Module module() { + return new CacheModule() { + @Override + protected void configure() { + persist(OAUTH_TOKENS, String.class, OAuthToken.class); + } + }; + } + + private final Cache cache; + + @Inject + OAuthTokenCache(@Named(OAUTH_TOKENS) Cache cache, + DynamicItem encrypter) { + this.cache = cache; + this.encrypter = encrypter; + } + + public boolean has(OAuthUserInfo user) { + return user != null + ? cache.getIfPresent(user.getUserName()) != null + : false; + } + + public OAuthToken get(OAuthUserInfo user) { + return user != null + ? get(user.getUserName()) + : null; + } + + public OAuthToken get(String userName) { + OAuthToken accessToken = cache.getIfPresent(userName); + if (accessToken == null) { + return null; + } + accessToken = decrypt(accessToken); + if (accessToken.isExpired()) { + cache.invalidate(userName); + return null; + } + return accessToken; + } + + public void put(OAuthUserInfo user, OAuthToken accessToken) { + cache.put(checkNotNull(user.getUserName()), + encrypt(checkNotNull(accessToken))); + } + + public void remove(OAuthUserInfo user) { + if (user != null) { + cache.invalidate(user.getUserName()); + } + } + + private OAuthToken encrypt(OAuthToken token) { + OAuthTokenEncrypter enc = encrypter.get(); + if (enc == null) { + return token; + } + return enc.encrypt(token); + } + + private OAuthToken decrypt(OAuthToken token) { + OAuthTokenEncrypter enc = encrypter.get(); + if (enc == null) { + return token; + } + return enc.decrypt(token); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index b5b3b6adaa..9cce3e706e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -21,6 +21,7 @@ import com.google.gerrit.audit.AuditModule; import com.google.gerrit.common.EventListener; import com.google.gerrit.common.UserScopedEventListener; import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider; +import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter; import com.google.gerrit.extensions.config.CapabilityDefinition; import com.google.gerrit.extensions.config.CloneCommand; import com.google.gerrit.extensions.config.DownloadCommand; @@ -74,6 +75,7 @@ import com.google.gerrit.server.account.VersionedAuthorizedKeys; import com.google.gerrit.server.api.accounts.AccountExternalIdCreator; import com.google.gerrit.server.auth.AuthBackend; import com.google.gerrit.server.auth.UniversalAuthBackend; +import com.google.gerrit.server.auth.oauth.OAuthTokenCache; import com.google.gerrit.server.avatar.AvatarProvider; import com.google.gerrit.server.cache.CacheRemovalListener; import com.google.gerrit.server.change.ChangeJson; @@ -191,6 +193,7 @@ public class GerritGlobalModule extends FactoryModule { install(SectionSortCache.module()); install(SubmitStrategy.module()); install(TagCache.module()); + install(OAuthTokenCache.module()); install(new AccessControlModule()); install(new CmdLineParserModule()); @@ -315,6 +318,7 @@ public class GerritGlobalModule extends FactoryModule { DynamicSet.setOf(binder(), ProjectWebLink.class); DynamicSet.setOf(binder(), BranchWebLink.class); DynamicMap.mapOf(binder(), OAuthLoginProvider.class); + DynamicItem.itemOf(binder(), OAuthTokenEncrypter.class); DynamicSet.setOf(binder(), AccountExternalIdCreator.class); DynamicSet.setOf(binder(), WebUiPlugin.class);