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:
parent
c96c6c4feb
commit
6067f93984
13 changed files with 161 additions and 221 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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("");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -2217,7 +2217,7 @@
|
|||
"realm" : "Migration",
|
||||
"notBefore" : 0,
|
||||
"defaultSignatureAlgorithm" : "RS256",
|
||||
"revokeRefreshToken" : false,
|
||||
"revokeRefreshToken" : true,
|
||||
"refreshTokenMaxReuse" : 0,
|
||||
"accessTokenLifespan" : 300,
|
||||
"accessTokenLifespanForImplicitFlow" : 900,
|
||||
|
|
Loading…
Reference in a new issue