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:
* `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);
}
@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
public String getAction() {
return entity.getAction();
@ -329,8 +283,6 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
UserSessionModel userSession = getUserSession();
entity.setAction(null);
entity.setRedirectUri(null);
entity.setCurrentRefreshToken(null);
entity.setCurrentRefreshTokenUseCount(-1);
entity.setTimestamp(Time.currentTime());
entity.getNotes().clear();
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.setRedirectUri(clientSession.getRedirectUri());
entity.setTimestamp(clientSession.getTimestamp());
entity.setCurrentRefreshToken(clientSession.getCurrentRefreshToken());
entity.setCurrentRefreshTokenUseCount(clientSession.getCurrentRefreshTokenUseCount());
entity.setOffline(offline);
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
public String getNote(String name) {
return entity.getNotes().get(name);
@ -310,16 +290,6 @@ public class JpaChangesPerformer<K, V extends SessionEntity> implements SessionC
clientSessionModel.setTimestamp(timestamp);
}
@Override
public void setCurrentRefreshToken(String currentRefreshToken) {
clientSessionModel.setCurrentRefreshToken(currentRefreshToken);
}
@Override
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
clientSessionModel.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
}
@Override
public void setAction(String 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));
}
@Override
public String getCurrentRefreshToken() {
return clientSessionModel.getCurrentRefreshToken();
}
@Override
public int getCurrentRefreshTokenUseCount() {
return clientSessionModel.getCurrentRefreshTokenUseCount();
}
@Override
public UUID 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 String currentRefreshToken;
private int currentRefreshTokenUseCount;
private final UUID id;
private transient String userSessionId;
@ -125,22 +122,6 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
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() {
return id;
}
@ -214,8 +195,6 @@ public class AuthenticatedClientSessionEntity extends SessionEntity {
Map<String, String> notes = session.getNotes();
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<>());
sessionEntity.setNotes(notes);
sessionEntity.setCurrentRefreshToken(MarshallUtil.unmarshallString(input));
sessionEntity.setCurrentRefreshTokenUseCount(KeycloakMarshallUtil.unmarshallInteger(input));
return sessionEntity;
}

View file

@ -17,6 +17,7 @@
package org.keycloak.models.session;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@ -188,26 +189,6 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
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
public String getAction() {
return getData().getAction();
@ -277,6 +258,7 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
return getId();
}
@JsonIgnoreProperties(ignoreUnknown = true)
protected static class PersistentClientSessionData {
@JsonProperty("authMethod")
@ -302,10 +284,6 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
private Set<String> protocolMappers;
@JsonProperty("roles")
private Set<String> roles;
@JsonProperty("currentRefreshToken")
private String currentRefreshToken;
@JsonProperty("currentRefreshTokenUseCount")
private int currentRefreshTokenUseCount;
public String getAuthMethod() {
return authMethod;
@ -379,20 +357,5 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
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 TAB_ID = "tab_id";
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 KEY = "key";

View file

@ -31,6 +31,9 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
final String STARTED_AT_NOTE = "startedAt";
final String USER_SESSION_STARTED_AT_NOTE = "userSessionStartedAt";
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();
@ -63,11 +66,56 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
void detachFromUserSession();
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);
void setNote(String name, String value);
@ -77,8 +125,6 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
default void restartClientSession() {
setAction(null);
setRedirectUri(null);
setCurrentRefreshToken(null);
setCurrentRefreshTokenUseCount(-1);
setTimestamp(Time.currentTime());
for (String note : getNotes().keySet()) {
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);
if (clientConfig.isUseRefreshToken()) {
//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
@ -442,8 +442,9 @@ public class TokenManager {
AuthenticatedClientSessionModel clientSession = validation.clientSessionCtx.getClientSession();
try {
validateTokenReuse(session, realm, refreshToken, clientSession, true);
int currentCount = clientSession.getCurrentRefreshTokenUseCount();
clientSession.setCurrentRefreshTokenUseCount(currentCount + 1);
String key = getReuseIdKey(refreshToken);
int currentCount = clientSession.getRefreshTokenUseCount(key);
clientSession.setRefreshTokenUseCount(key, currentCount + 1);
} catch (OAuthErrorException oee) {
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",
@ -456,27 +457,27 @@ public class TokenManager {
}
// Will throw OAuthErrorException if validation fails
private void validateTokenReuse(KeycloakSession session, RealmModel realm, AccessToken refreshToken,
AuthenticatedClientSessionModel clientSession, boolean refreshFlag) throws OAuthErrorException {
private void validateTokenReuse(KeycloakSession session, RealmModel realm, AccessToken refreshToken, AuthenticatedClientSessionModel clientSession, boolean refreshFlag) throws OAuthErrorException {
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
&& !refreshToken.getId().equals(clientSession.getCurrentRefreshToken())
&& refreshToken.getIat() < clientSession.getTimestamp()
&& startupTime <= clientSession.getTimestamp()) {
//check if a more recent refresh token is already used on this tab, if yes the refresh token is invalid
if (refreshTokenId != null && !refreshToken.getId().equals(refreshTokenId) && refreshToken.getIat() < lastRefresh && startupTime <= lastRefresh) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
}
if (!refreshToken.getId().equals(clientSession.getCurrentRefreshToken())) {
if (!refreshToken.getId().equals(refreshTokenId)) {
if (refreshFlag) {
clientSession.setCurrentRefreshToken(refreshToken.getId());
clientSession.setCurrentRefreshTokenUseCount(0);
clientSession.setRefreshToken(key, refreshToken.getId());
clientSession.setRefreshTokenUseCount(key, 0);
} else {
return;
}
}
int currentCount = clientSession.getCurrentRefreshTokenUseCount();
int currentCount = clientSession.getRefreshTokenUseCount(key);
if (currentCount > realm.getRefreshTokenMaxReuse()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "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.setTimestamp(userSession.getLastSessionRefresh());
// 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);
@ -1104,18 +1104,27 @@ public class TokenManager {
boolean offlineTokenRequested = offlineAccessScope==null ? false : clientSessionCtx.getClientScopeIds().contains(offlineAccessScope.getId());
generateRefreshToken(offlineTokenRequested);
refreshToken.setScope(clientSessionCtx.getScopeString(true));
if (realm.isRevokeRefreshToken()) {
refreshToken.getOtherClaims().put(Constants.REUSE_ID, KeycloakModelUtils.generateId());
}
return this;
}
public AccessTokenResponseBuilder generateRefreshToken(String scope, AuthenticatedClientSessionModel clientSession) {
public AccessTokenResponseBuilder generateRefreshToken(RefreshToken oldRefreshToken, AuthenticatedClientSessionModel clientSession) {
if (accessToken == null) {
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) ;
if (offlineTokenRequested)
clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, scope, session);
generateRefreshToken(offlineTokenRequested);
if (realm.isRevokeRefreshToken()) {
refreshToken.getOtherClaims().put(Constants.REUSE_ID, reuseId);
clientSession.setRefreshTokenLastRefresh(getReuseIdKey(oldRefreshToken), refreshToken.getIat().intValue());
}
refreshToken.setScope(scope);
return this;
}
@ -1124,12 +1133,10 @@ public class TokenManager {
refreshToken = new RefreshToken(accessToken);
refreshToken.id(KeycloakModelUtils.generateId());
refreshToken.issuedNow();
int currentTime = Time.currentTime();
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
clientSession.setTimestamp(currentTime);
clientSession.setTimestamp(refreshToken.getIat().intValue());
UserSessionModel userSession = clientSession.getUserSession();
userSession.setLastSessionRefresh(currentTime);
userSession.setLastSessionRefresh(refreshToken.getIat().intValue());
if (offlineTokenRequested) {
UserSessionManager sessionManager = new UserSessionManager(session);
if (!sessionManager.isOfflineTokenAllowed(clientSessionCtx)) {
@ -1462,4 +1469,8 @@ public class TokenManager {
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;
}
@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
public String getNote(String name) {
return notes.get(name);

View file

@ -37,26 +37,6 @@ class TestAuthenticatedClientSessionModel implements AuthenticatedClientSessionM
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
public String getNote(String name) {
return notes.get(name);

View file

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import org.htmlunit.WebClient;
import java.io.Closeable;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.jboss.arquillian.drone.webdriver.htmlunit.DroneHtmlUnitDriver;
import org.jboss.arquillian.graphene.page.Page;
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.UserManager;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.testsuite.util.BrowserTabUtil;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
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
public void refreshTokenReuseTokenWithRefreshTokensRevokedAfterSingleReuse() throws Exception {
try {

View file

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