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 <michael.ochmann@sap.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
@@ -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> 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> webSession,
|
||||
Provider<IdentifiedUser> 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) {
|
||||
|
@@ -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<OAuthTokenEncrypter> encrypter;
|
||||
|
||||
public static Module module() {
|
||||
return new CacheModule() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
persist(OAUTH_TOKENS, String.class, OAuthToken.class);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private final Cache<String, OAuthToken> cache;
|
||||
|
||||
@Inject
|
||||
OAuthTokenCache(@Named(OAUTH_TOKENS) Cache<String, OAuthToken> cache,
|
||||
DynamicItem<OAuthTokenEncrypter> 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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user