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:
|
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.
|
||||||
|
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
@ -743,7 +743,7 @@ public class TokenManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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("");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue