Improvements to refresh token rotation with multiple tabs (#29966)

Closes #14122

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
Giuseppe Graziano 2024-06-07 12:02:36 +02:00 committed by GitHub
parent c96c6c4feb
commit 6067f93984
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 161 additions and 221 deletions

View file

@ -461,3 +461,9 @@ The following variables were deprecated in the Admin theme:
The following variables from the environment script injected into the page of the Admin theme are deprecated: The following variables from the environment script injected into the page of the Admin theme are deprecated:
* `authUrl`. Do not use this variable. * `authUrl`. Do not use this variable.
= Methods to get and set current refresh token in client session are now deprecated
The methods `String getCurrentRefreshToken()`, `void setCurrentRefreshToken(String currentRefreshToken)`, `int getCurrentRefreshTokenUseCount()`, and `void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount)` in the interface `org.keycloak.models.AuthenticatedClientSessionModel` are deprecated. They have been replaced by similar methods that require an identifier as a parameter such as `getRefreshToken(String reuseId)` to manage multiple refresh tokens within a client session.
The methods will be removed in one of the future {project_name} releases. It might be {project_name} 27 release.

View file

@ -176,52 +176,6 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
update(task); update(task);
} }
@Override
public int getCurrentRefreshTokenUseCount() {
return entity.getCurrentRefreshTokenUseCount();
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
@Override
public void runUpdate(AuthenticatedClientSessionEntity entity) {
entity.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
}
@Override
public boolean isOffline() {
return offline;
}
};
update(task);
}
@Override
public String getCurrentRefreshToken() {
return entity.getCurrentRefreshToken();
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
ClientSessionUpdateTask task = new ClientSessionUpdateTask() {
@Override
public void runUpdate(AuthenticatedClientSessionEntity entity) {
entity.setCurrentRefreshToken(currentRefreshToken);
}
@Override
public boolean isOffline() {
return offline;
}
};
update(task);
}
@Override @Override
public String getAction() { public String getAction() {
return entity.getAction(); return entity.getAction();
@ -329,8 +283,6 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
UserSessionModel userSession = getUserSession(); UserSessionModel userSession = getUserSession();
entity.setAction(null); entity.setAction(null);
entity.setRedirectUri(null); entity.setRedirectUri(null);
entity.setCurrentRefreshToken(null);
entity.setCurrentRefreshTokenUseCount(-1);
entity.setTimestamp(Time.currentTime()); entity.setTimestamp(Time.currentTime());
entity.getNotes().clear(); entity.getNotes().clear();
entity.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(entity.getTimestamp())); entity.getNotes().put(AuthenticatedClientSessionModel.STARTED_AT_NOTE, String.valueOf(entity.getTimestamp()));

View file

@ -956,8 +956,6 @@ public class PersistentUserSessionProvider implements UserSessionProvider, Sessi
entity.setClientId(clientId); entity.setClientId(clientId);
entity.setRedirectUri(clientSession.getRedirectUri()); entity.setRedirectUri(clientSession.getRedirectUri());
entity.setTimestamp(clientSession.getTimestamp()); entity.setTimestamp(clientSession.getTimestamp());
entity.setCurrentRefreshToken(clientSession.getCurrentRefreshToken());
entity.setCurrentRefreshTokenUseCount(clientSession.getCurrentRefreshTokenUseCount());
entity.setOffline(offline); entity.setOffline(offline);
return entity; return entity;

View file

@ -181,26 +181,6 @@ public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionC
}; };
} }
@Override
public String getCurrentRefreshToken() {
return entity.getCurrentRefreshToken();
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
throw new IllegalStateException("not implemented");
}
@Override
public int getCurrentRefreshTokenUseCount() {
return entity.getCurrentRefreshTokenUseCount();
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
throw new IllegalStateException("not implemented");
}
@Override @Override
public String getNote(String name) { public String getNote(String name) {
return entity.getNotes().get(name); return entity.getNotes().get(name);
@ -310,16 +290,6 @@ public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionC
clientSessionModel.setTimestamp(timestamp); clientSessionModel.setTimestamp(timestamp);
} }
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
clientSessionModel.setCurrentRefreshToken(currentRefreshToken);
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
clientSessionModel.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
}
@Override @Override
public void setAction(String action) { public void setAction(String action) {
clientSessionModel.setAction(action); clientSessionModel.setAction(action);
@ -381,16 +351,6 @@ public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionC
notes.forEach((k, v) -> clientSessionModel.setNote(k, v)); notes.forEach((k, v) -> clientSessionModel.setNote(k, v));
} }
@Override
public String getCurrentRefreshToken() {
return clientSessionModel.getCurrentRefreshToken();
}
@Override
public int getCurrentRefreshTokenUseCount() {
return clientSessionModel.getCurrentRefreshTokenUseCount();
}
@Override @Override
public UUID getId() { public UUID getId() {
return UUID.fromString(clientSessionModel.getId()); return UUID.fromString(clientSessionModel.getId());

View file

@ -52,9 +52,6 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
private Map<String, String> notes = new ConcurrentHashMap<>(); private Map<String, String> notes = new ConcurrentHashMap<>();
private String currentRefreshToken;
private int currentRefreshTokenUseCount;
private final UUID id; private final UUID id;
private transient String userSessionId; private transient String userSessionId;
@ -125,22 +122,6 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
this.notes = notes; this.notes = notes;
} }
public String getCurrentRefreshToken() {
return currentRefreshToken;
}
public void setCurrentRefreshToken(String currentRefreshToken) {
this.currentRefreshToken = currentRefreshToken;
}
public int getCurrentRefreshTokenUseCount() {
return currentRefreshTokenUseCount;
}
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
this.currentRefreshTokenUseCount = currentRefreshTokenUseCount;
}
public UUID getId() { public UUID getId() {
return id; return id;
} }
@ -214,8 +195,6 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
Map<String, String> notes = session.getNotes(); Map<String, String> notes = session.getNotes();
KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output); KeycloakMarshallUtil.writeMap(notes, KeycloakMarshallUtil.STRING_EXT, KeycloakMarshallUtil.STRING_EXT, output);
MarshallUtil.marshallString(session.getCurrentRefreshToken(), output);
KeycloakMarshallUtil.marshall(session.getCurrentRefreshTokenUseCount(), output);
} }
@ -234,9 +213,6 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>()); new KeycloakMarshallUtil.ConcurrentHashMapBuilder<>());
sessionEntity.setNotes(notes); sessionEntity.setNotes(notes);
sessionEntity.setCurrentRefreshToken(MarshallUtil.unmarshallString(input));
sessionEntity.setCurrentRefreshTokenUseCount(KeycloakMarshallUtil.unmarshallInteger(input));
return sessionEntity; return sessionEntity;
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.models.session; package org.keycloak.models.session;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -188,26 +189,6 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
model.setTimestamp(timestamp); model.setTimestamp(timestamp);
} }
@Override
public String getCurrentRefreshToken() {
return getData().getCurrentRefreshToken();
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
getData().setCurrentRefreshToken(currentRefreshToken);
}
@Override
public int getCurrentRefreshTokenUseCount() {
return getData().getCurrentRefreshTokenUseCount();
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
getData().setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
}
@Override @Override
public String getAction() { public String getAction() {
return getData().getAction(); return getData().getAction();
@ -277,6 +258,7 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
return getId(); return getId();
} }
@JsonIgnoreProperties(ignoreUnknown = true)
protected static class PersistentClientSessionData { protected static class PersistentClientSessionData {
@JsonProperty("authMethod") @JsonProperty("authMethod")
@ -302,10 +284,6 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
private Set<String> protocolMappers; private Set<String> protocolMappers;
@JsonProperty("roles") @JsonProperty("roles")
private Set<String> roles; private Set<String> roles;
@JsonProperty("currentRefreshToken")
private String currentRefreshToken;
@JsonProperty("currentRefreshTokenUseCount")
private int currentRefreshTokenUseCount;
public String getAuthMethod() { public String getAuthMethod() {
return authMethod; return authMethod;
@ -379,20 +357,5 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
this.roles = roles; this.roles = roles;
} }
public String getCurrentRefreshToken() {
return currentRefreshToken;
}
public void setCurrentRefreshToken(String currentRefreshToken) {
this.currentRefreshToken = currentRefreshToken;
}
public int getCurrentRefreshTokenUseCount() {
return currentRefreshTokenUseCount;
}
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
this.currentRefreshTokenUseCount = currentRefreshTokenUseCount;
}
} }
} }

View file

@ -82,7 +82,7 @@ public final class Constants {
public static final String TOKEN = "token"; public static final String TOKEN = "token";
public static final String TAB_ID = "tab_id"; public static final String TAB_ID = "tab_id";
public static final String CLIENT_DATA = "client_data"; public static final String CLIENT_DATA = "client_data";
public static final String REUSE_ID = "reuse_id";
public static final String SKIP_LOGOUT = "skip_logout"; public static final String SKIP_LOGOUT = "skip_logout";
public static final String KEY = "key"; public static final String KEY = "key";

View file

@ -31,6 +31,9 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
final String STARTED_AT_NOTE = "startedAt"; final String STARTED_AT_NOTE = "startedAt";
final String USER_SESSION_STARTED_AT_NOTE = "userSessionStartedAt"; final String USER_SESSION_STARTED_AT_NOTE = "userSessionStartedAt";
final String USER_SESSION_REMEMBER_ME_NOTE = "userSessionRememberMe"; final String USER_SESSION_REMEMBER_ME_NOTE = "userSessionRememberMe";
final String REFRESH_TOKEN_PREFIX = "refreshTokenPrefix";
final String REFRESH_TOKEN_USE_PREFIX = "refreshTokenUsePrefix";
final String REFRESH_TOKEN_LAST_REFRESH_PREFIX = "refreshTokenLastRefreshPrefix";
String getId(); String getId();
@ -63,11 +66,56 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
void detachFromUserSession(); void detachFromUserSession();
UserSessionModel getUserSession(); UserSessionModel getUserSession();
String getCurrentRefreshToken(); /**
void setCurrentRefreshToken(String currentRefreshToken); * @deprecated use {@link #getRefreshToken(String)}
*/
@Deprecated
default String getCurrentRefreshToken() {
return null;
}
int getCurrentRefreshTokenUseCount(); /**
void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount); * @deprecated use {@link #setRefreshToken(String, String)}}
*/
@Deprecated
default void setCurrentRefreshToken(String currentRefreshToken) {
}
/**
* @deprecated use {@link #getRefreshTokenUseCount(String)}
*/
@Deprecated
default int getCurrentRefreshTokenUseCount() {
return 0;
}
/**
* deprecated use {@link #setRefreshTokenUseCount(String, int)}
*/
@Deprecated
default void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
}
default String getRefreshToken(String reuseId) {
return getNote(REFRESH_TOKEN_PREFIX + reuseId);
}
default void setRefreshToken(String reuseId, String refreshTokenId) {
setNote(REFRESH_TOKEN_PREFIX + reuseId, refreshTokenId);
}
default int getRefreshTokenUseCount(String reuseId) {
String count = getNote(REFRESH_TOKEN_USE_PREFIX + reuseId);
return count == null ? 0 : Integer.parseInt(count);
}
default void setRefreshTokenUseCount(String reuseId, int refreshTokenUseCount) {
setNote(REFRESH_TOKEN_USE_PREFIX + reuseId, String.valueOf(refreshTokenUseCount));
}
default int getRefreshTokenLastRefresh(String reuseId) {
String timestamp = getNote(REFRESH_TOKEN_LAST_REFRESH_PREFIX + reuseId);
return timestamp == null ? 0 : Integer.parseInt(timestamp);
}
default void setRefreshTokenLastRefresh(String reuseId, int refreshTokenLastRefresh) {
setNote(REFRESH_TOKEN_LAST_REFRESH_PREFIX + reuseId, String.valueOf(refreshTokenLastRefresh));
}
String getNote(String name); String getNote(String name);
void setNote(String name, String value); void setNote(String name, String value);
@ -77,8 +125,6 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
default void restartClientSession() { default void restartClientSession() {
setAction(null); setAction(null);
setRedirectUri(null); setRedirectUri(null);
setCurrentRefreshToken(null);
setCurrentRefreshTokenUseCount(-1);
setTimestamp(Time.currentTime()); setTimestamp(Time.currentTime());
for (String note : getNotes().keySet()) { for (String note : getNotes().keySet()) {
if (!AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE.equals(note) if (!AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE.equals(note)

View file

@ -420,7 +420,7 @@ public class TokenManager {
validation.userSession, validation.clientSessionCtx).accessToken(validation.newToken); validation.userSession, validation.clientSessionCtx).accessToken(validation.newToken);
if (clientConfig.isUseRefreshToken()) { if (clientConfig.isUseRefreshToken()) {
//refresh token must have same scope as old refresh token (type, scope, expiration) //refresh token must have same scope as old refresh token (type, scope, expiration)
responseBuilder.generateRefreshToken(refreshToken.getScope(), clientSession); responseBuilder.generateRefreshToken(refreshToken, clientSession);
} }
if (validation.newToken.getAuthorization() != null if (validation.newToken.getAuthorization() != null
@ -442,8 +442,9 @@ public class TokenManager {
AuthenticatedClientSessionModel clientSession = validation.clientSessionCtx.getClientSession(); AuthenticatedClientSessionModel clientSession = validation.clientSessionCtx.getClientSession();
try { try {
validateTokenReuse(session, realm, refreshToken, clientSession, true); validateTokenReuse(session, realm, refreshToken, clientSession, true);
int currentCount = clientSession.getCurrentRefreshTokenUseCount(); String key = getReuseIdKey(refreshToken);
clientSession.setCurrentRefreshTokenUseCount(currentCount + 1); int currentCount = clientSession.getRefreshTokenUseCount(key);
clientSession.setRefreshTokenUseCount(key, currentCount + 1);
} catch (OAuthErrorException oee) { } catch (OAuthErrorException oee) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debugf("Failed validation of refresh token %s due it was used before. Realm: %s, client: %s, user: %s, user session: %s. Will detach client session from user session", logger.debugf("Failed validation of refresh token %s due it was used before. Realm: %s, client: %s, user: %s, user session: %s. Will detach client session from user session",
@ -456,27 +457,27 @@ public class TokenManager {
} }
// Will throw OAuthErrorException if validation fails // Will throw OAuthErrorException if validation fails
private void validateTokenReuse(KeycloakSession session, RealmModel realm, AccessToken refreshToken, private void validateTokenReuse(KeycloakSession session, RealmModel realm, AccessToken refreshToken, AuthenticatedClientSessionModel clientSession, boolean refreshFlag) throws OAuthErrorException {
AuthenticatedClientSessionModel clientSession, boolean refreshFlag) throws OAuthErrorException {
int startupTime = session.getProvider(UserSessionProvider.class).getStartupTime(realm); int startupTime = session.getProvider(UserSessionProvider.class).getStartupTime(realm);
String key = getReuseIdKey(refreshToken);
String refreshTokenId = clientSession.getRefreshToken(key);
int lastRefresh = clientSession.getRefreshTokenLastRefresh(key);
if (clientSession.getCurrentRefreshToken() != null //check if a more recent refresh token is already used on this tab, if yes the refresh token is invalid
&& !refreshToken.getId().equals(clientSession.getCurrentRefreshToken()) if (refreshTokenId != null && !refreshToken.getId().equals(refreshTokenId) && refreshToken.getIat() < lastRefresh && startupTime <= lastRefresh) {
&& refreshToken.getIat() < clientSession.getTimestamp()
&& startupTime <= clientSession.getTimestamp()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
} }
if (!refreshToken.getId().equals(clientSession.getCurrentRefreshToken())) { if (!refreshToken.getId().equals(refreshTokenId)) {
if (refreshFlag) { if (refreshFlag) {
clientSession.setCurrentRefreshToken(refreshToken.getId()); clientSession.setRefreshToken(key, refreshToken.getId());
clientSession.setCurrentRefreshTokenUseCount(0); clientSession.setRefreshTokenUseCount(key, 0);
} else { } else {
return; return;
} }
} }
int currentCount = clientSession.getCurrentRefreshTokenUseCount(); int currentCount = clientSession.getRefreshTokenUseCount(key);
if (currentCount > realm.getRefreshTokenMaxReuse()) { if (currentCount > realm.getRefreshTokenMaxReuse()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Maximum allowed refresh token reuse exceeded", throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Maximum allowed refresh token reuse exceeded",
"Maximum allowed refresh token reuse exceeded"); "Maximum allowed refresh token reuse exceeded");
@ -604,7 +605,6 @@ public class TokenManager {
clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(new AcrStore(session, authSession).getLevelOfAuthenticationFromCurrentAuthentication())); clientSession.setNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(new AcrStore(session, authSession).getLevelOfAuthenticationFromCurrentAuthentication()));
clientSession.setTimestamp(userSession.getLastSessionRefresh()); clientSession.setTimestamp(userSession.getLastSessionRefresh());
// Remove authentication session now (just current tab, not whole "rootAuthenticationSession" in case we have more browser tabs with "authentications in progress") // Remove authentication session now (just current tab, not whole "rootAuthenticationSession" in case we have more browser tabs with "authentications in progress")
new AuthenticationSessionManager(session).updateAuthenticationSessionAfterSuccessfulAuthentication(userSession.getRealm(), authSession); new AuthenticationSessionManager(session).updateAuthenticationSessionAfterSuccessfulAuthentication(userSession.getRealm(), authSession);
@ -1104,18 +1104,27 @@ public class TokenManager {
boolean offlineTokenRequested = offlineAccessScope==null ? false : clientSessionCtx.getClientScopeIds().contains(offlineAccessScope.getId()); boolean offlineTokenRequested = offlineAccessScope==null ? false : clientSessionCtx.getClientScopeIds().contains(offlineAccessScope.getId());
generateRefreshToken(offlineTokenRequested); generateRefreshToken(offlineTokenRequested);
refreshToken.setScope(clientSessionCtx.getScopeString(true)); refreshToken.setScope(clientSessionCtx.getScopeString(true));
if (realm.isRevokeRefreshToken()) {
refreshToken.getOtherClaims().put(Constants.REUSE_ID, KeycloakModelUtils.generateId());
}
return this; return this;
} }
public AccessTokenResponseBuilder generateRefreshToken(String scope, AuthenticatedClientSessionModel clientSession) { public AccessTokenResponseBuilder generateRefreshToken(RefreshToken oldRefreshToken, AuthenticatedClientSessionModel clientSession) {
if (accessToken == null) { if (accessToken == null) {
throw new IllegalStateException("accessToken not set"); throw new IllegalStateException("accessToken not set");
} }
String scope = oldRefreshToken.getScope();
Object reuseId = oldRefreshToken.getOtherClaims().get(Constants.REUSE_ID);
boolean offlineTokenRequested = Arrays.asList(scope.split(" ")).contains(OAuth2Constants.OFFLINE_ACCESS) ; boolean offlineTokenRequested = Arrays.asList(scope.split(" ")).contains(OAuth2Constants.OFFLINE_ACCESS) ;
if (offlineTokenRequested) if (offlineTokenRequested)
clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, scope, session); clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, scope, session);
generateRefreshToken(offlineTokenRequested); generateRefreshToken(offlineTokenRequested);
if (realm.isRevokeRefreshToken()) {
refreshToken.getOtherClaims().put(Constants.REUSE_ID, reuseId);
clientSession.setRefreshTokenLastRefresh(getReuseIdKey(oldRefreshToken), refreshToken.getIat().intValue());
}
refreshToken.setScope(scope); refreshToken.setScope(scope);
return this; return this;
} }
@ -1124,12 +1133,10 @@ public class TokenManager {
refreshToken = new RefreshToken(accessToken); refreshToken = new RefreshToken(accessToken);
refreshToken.id(KeycloakModelUtils.generateId()); refreshToken.id(KeycloakModelUtils.generateId());
refreshToken.issuedNow(); refreshToken.issuedNow();
int currentTime = Time.currentTime();
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession(); AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
clientSession.setTimestamp(currentTime); clientSession.setTimestamp(refreshToken.getIat().intValue());
UserSessionModel userSession = clientSession.getUserSession(); UserSessionModel userSession = clientSession.getUserSession();
userSession.setLastSessionRefresh(currentTime); userSession.setLastSessionRefresh(refreshToken.getIat().intValue());
if (offlineTokenRequested) { if (offlineTokenRequested) {
UserSessionManager sessionManager = new UserSessionManager(session); UserSessionManager sessionManager = new UserSessionManager(session);
if (!sessionManager.isOfflineTokenAllowed(clientSessionCtx)) { if (!sessionManager.isOfflineTokenAllowed(clientSessionCtx)) {
@ -1462,4 +1469,8 @@ public class TokenManager {
return false; return false;
} }
private String getReuseIdKey(AccessToken refreshToken) {
return Optional.ofNullable(refreshToken.getOtherClaims().get(Constants.REUSE_ID)).map(String::valueOf).orElse("");
}
} }

View file

@ -37,26 +37,6 @@ public class TestAuthenticatedClientSessionModel implements AuthenticatedClientS
return null; return null;
} }
@Override
public String getCurrentRefreshToken() {
return null;
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
}
@Override
public int getCurrentRefreshTokenUseCount() {
return 0;
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
}
@Override @Override
public String getNote(String name) { public String getNote(String name) {
return notes.get(name); return notes.get(name);

View file

@ -37,26 +37,6 @@ class TestAuthenticatedClientSessionModel implements AuthenticatedClientSessionM
return null; return null;
} }
@Override
public String getCurrentRefreshToken() {
return null;
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
}
@Override
public int getCurrentRefreshTokenUseCount() {
return 0;
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
}
@Override @Override
public String getNote(String name) { public String getNote(String name) {
return notes.get(name); return notes.get(name);

View file

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import org.htmlunit.WebClient; import org.htmlunit.WebClient;
import java.io.Closeable; import java.io.Closeable;
import org.hamcrest.CoreMatchers; import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.jboss.arquillian.drone.webdriver.htmlunit.DroneHtmlUnitDriver; import org.jboss.arquillian.drone.webdriver.htmlunit.DroneHtmlUnitDriver;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert; import org.junit.Assert;
@ -78,6 +79,7 @@ import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.UserInfoClientUtil; import org.keycloak.testsuite.util.UserInfoClientUtil;
import org.keycloak.testsuite.util.UserManager; import org.keycloak.testsuite.util.UserManager;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.util.BrowserTabUtil;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.openqa.selenium.Cookie; import org.openqa.selenium.Cookie;
@ -667,6 +669,72 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
} }
} }
@Test
public void refreshTokenReuseOnDifferentTab() {
try {
BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver);
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true);
//login with tab 1
oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password");
RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
events.expectCodeToToken(codeId, sessionId).assertEvent();
assertNotNull(refreshToken1.getOtherClaims().get(Constants.REUSE_ID));
assertNotEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshToken1.getId());
//login with tab 2
tabUtil.newTab(oauth.getLoginFormUrl());
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
loginEvent = events.expectLogin().assertEvent();
sessionId = loginEvent.getSessionId();
codeId = loginEvent.getDetails().get(Details.CODE_ID);
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse responseNew = oauth.doAccessTokenRequest(code, "password");
RefreshToken refreshTokenNew = oauth.parseRefreshToken(responseNew.getRefreshToken());
events.expectCodeToToken(codeId, sessionId).assertEvent();
assertNotNull(refreshToken1.getOtherClaims().get(Constants.REUSE_ID));
assertNotEquals(refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew.getId());
assertNotEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID));
setTimeOffset(10);
//refresh with token from tab 1
OAuthClient.AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
assertEquals(200, response2.getStatusCode());
RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
events.expectRefresh(refreshToken2.getId(), sessionId);
assertNotEquals(refreshToken2.getOtherClaims().get(Constants.REUSE_ID), refreshToken2.getId());
assertEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshToken2.getOtherClaims().get(Constants.REUSE_ID));
//refresh with token from tab 2
OAuthClient.AccessTokenResponse responseNew1 = oauth.doRefreshTokenRequest(responseNew.getRefreshToken(), "password");
assertEquals(200, responseNew1.getStatusCode());
RefreshToken refreshTokenNew1 = oauth.parseRefreshToken(responseNew1.getRefreshToken());
events.expectRefresh(refreshTokenNew1.getId(), sessionId);
assertNotEquals(refreshTokenNew1.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew1.getId());
assertEquals(refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew1.getOtherClaims().get(Constants.REUSE_ID));
//try refresh token reuse with token from tab 2
responseNew1 = oauth.doRefreshTokenRequest(responseNew.getRefreshToken(), "password");
assertEquals(400, responseNew1.getStatusCode());
} finally {
resetTimeOffset();
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
}
}
@Test @Test
public void refreshTokenReuseTokenWithRefreshTokensRevokedAfterSingleReuse() throws Exception { public void refreshTokenReuseTokenWithRefreshTokensRevokedAfterSingleReuse() throws Exception {
try { try {

View file

@ -2217,7 +2217,7 @@
"realm" : "Migration", "realm" : "Migration",
"notBefore" : 0, "notBefore" : 0,
"defaultSignatureAlgorithm" : "RS256", "defaultSignatureAlgorithm" : "RS256",
"revokeRefreshToken" : false, "revokeRefreshToken" : true,
"refreshTokenMaxReuse" : 0, "refreshTokenMaxReuse" : 0,
"accessTokenLifespan" : 300, "accessTokenLifespan" : 300,
"accessTokenLifespanForImplicitFlow" : 900, "accessTokenLifespanForImplicitFlow" : 900,