KEYCLOAK-3303: Allow reuse of refresh tokens.
- Configurable max reuse count.
This commit is contained in:
parent
4c71e2ec17
commit
134daeac7f
19 changed files with 392 additions and 14 deletions
|
@ -38,6 +38,7 @@ public class RealmRepresentation {
|
|||
protected String displayNameHtml;
|
||||
protected Integer notBefore;
|
||||
protected Boolean revokeRefreshToken;
|
||||
protected Integer refreshTokenMaxReuse;
|
||||
protected Integer accessTokenLifespan;
|
||||
protected Integer accessTokenLifespanForImplicitFlow;
|
||||
protected Integer ssoSessionIdleTimeout;
|
||||
|
@ -240,6 +241,14 @@ public class RealmRepresentation {
|
|||
this.revokeRefreshToken = revokeRefreshToken;
|
||||
}
|
||||
|
||||
public Integer getRefreshTokenMaxReuse() {
|
||||
return refreshTokenMaxReuse;
|
||||
}
|
||||
|
||||
public void setRefreshTokenMaxReuse(Integer refreshTokenMaxReuse) {
|
||||
this.refreshTokenMaxReuse = refreshTokenMaxReuse;
|
||||
}
|
||||
|
||||
public Integer getAccessTokenLifespan() {
|
||||
return accessTokenLifespan;
|
||||
}
|
||||
|
|
|
@ -357,6 +357,18 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
updated.setRevokeRefreshToken(revokeRefreshToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRefreshTokenMaxReuse() {
|
||||
if (isUpdated()) return updated.getRefreshTokenMaxReuse();
|
||||
return cached.getRefreshTokenMaxReuse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRefreshTokenMaxReuse(int refreshTokenMaxReuse) {
|
||||
getDelegateForUpdate();
|
||||
updated.setRefreshTokenMaxReuse(refreshTokenMaxReuse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSsoSessionIdleTimeout() {
|
||||
if (isUpdated()) return updated.getSsoSessionIdleTimeout();
|
||||
|
|
|
@ -75,6 +75,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
//--- end brute force settings
|
||||
|
||||
protected boolean revokeRefreshToken;
|
||||
protected int refreshTokenMaxReuse;
|
||||
protected int ssoSessionIdleTimeout;
|
||||
protected int ssoSessionMaxLifespan;
|
||||
protected int offlineSessionIdleTimeout;
|
||||
|
@ -170,6 +171,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
//--- end brute force settings
|
||||
|
||||
revokeRefreshToken = model.isRevokeRefreshToken();
|
||||
refreshTokenMaxReuse = model.getRefreshTokenMaxReuse();
|
||||
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
||||
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
||||
offlineSessionIdleTimeout = model.getOfflineSessionIdleTimeout();
|
||||
|
@ -374,6 +376,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
return revokeRefreshToken;
|
||||
}
|
||||
|
||||
public int getRefreshTokenMaxReuse() {
|
||||
return refreshTokenMaxReuse;
|
||||
}
|
||||
|
||||
public int getSsoSessionIdleTimeout() {
|
||||
return ssoSessionIdleTimeout;
|
||||
}
|
||||
|
|
|
@ -155,6 +155,56 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
|
|||
update(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentRefreshTokenUseCount() {
|
||||
return entity.getCurrentRefreshTokenUseCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
|
||||
UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
|
||||
|
||||
@Override
|
||||
protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
|
||||
entity.setCurrentRefreshTokenUseCount(currentRefreshTokenUseCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
|
||||
// We usually update lastSessionRefresh at the same time. That would handle it.
|
||||
return CrossDCMessageStatus.NOT_NEEDED;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
update(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentRefreshToken() {
|
||||
return entity.getCurrentRefreshToken();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentRefreshToken(String currentRefreshToken) {
|
||||
UserSessionClientSessionUpdateTask task = new UserSessionClientSessionUpdateTask(client.getId()) {
|
||||
|
||||
@Override
|
||||
protected void runClientSessionUpdate(AuthenticatedClientSessionEntity entity) {
|
||||
entity.setCurrentRefreshToken(currentRefreshToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrossDCMessageStatus getCrossDCMessageStatus(SessionEntityWrapper<UserSessionEntity> sessionWrapper) {
|
||||
// We usually update lastSessionRefresh at the same time. That would handle it.
|
||||
return CrossDCMessageStatus.NOT_NEEDED;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
update(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAction() {
|
||||
return entity.getAction();
|
||||
|
|
|
@ -46,6 +46,9 @@ public class AuthenticatedClientSessionEntity implements Serializable {
|
|||
private Set<String> protocolMappers;
|
||||
private Map<String, String> notes = new ConcurrentHashMap<>();
|
||||
|
||||
private String currentRefreshToken;
|
||||
private int currentRefreshTokenUseCount;
|
||||
|
||||
public String getAuthMethod() {
|
||||
return authMethod;
|
||||
}
|
||||
|
@ -102,6 +105,21 @@ public class AuthenticatedClientSessionEntity implements Serializable {
|
|||
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 static class ExternalizerImpl implements Externalizer<AuthenticatedClientSessionEntity> {
|
||||
|
||||
|
@ -117,6 +135,9 @@ public class AuthenticatedClientSessionEntity implements Serializable {
|
|||
|
||||
KeycloakMarshallUtil.writeCollection(session.getProtocolMappers(), KeycloakMarshallUtil.STRING_EXT, output);
|
||||
KeycloakMarshallUtil.writeCollection(session.getRoles(), KeycloakMarshallUtil.STRING_EXT, output);
|
||||
|
||||
MarshallUtil.marshallString(session.getCurrentRefreshToken(), output);
|
||||
MarshallUtil.marshallInt(output, session.getCurrentRefreshTokenUseCount());
|
||||
}
|
||||
|
||||
|
||||
|
@ -139,6 +160,9 @@ public class AuthenticatedClientSessionEntity implements Serializable {
|
|||
Set<String> roles = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, new KeycloakMarshallUtil.HashSetBuilder<>());
|
||||
sessionEntity.setRoles(roles);
|
||||
|
||||
sessionEntity.setCurrentRefreshToken(MarshallUtil.unmarshallString(input));
|
||||
sessionEntity.setCurrentRefreshTokenUseCount(MarshallUtil.unmarshallInt(input));
|
||||
|
||||
return sessionEntity;
|
||||
}
|
||||
|
||||
|
|
|
@ -390,6 +390,16 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
realm.setRevokeRefreshToken(revokeRefreshToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRefreshTokenMaxReuse() {
|
||||
return realm.getRefreshTokenMaxReuse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRefreshTokenMaxReuse(int revokeRefreshTokenReuseCount) {
|
||||
realm.setRefreshTokenMaxReuse(revokeRefreshTokenReuseCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessTokenLifespan() {
|
||||
return realm.getAccessTokenLifespan();
|
||||
|
|
|
@ -102,6 +102,8 @@ public class RealmEntity {
|
|||
|
||||
@Column(name="REVOKE_REFRESH_TOKEN")
|
||||
private boolean revokeRefreshToken;
|
||||
@Column(name="REFRESH_TOKEN_MAX_REUSE")
|
||||
private int refreshTokenMaxReuse;
|
||||
@Column(name="SSO_IDLE_TIMEOUT")
|
||||
private int ssoSessionIdleTimeout;
|
||||
@Column(name="SSO_MAX_LIFESPAN")
|
||||
|
@ -340,6 +342,14 @@ public class RealmEntity {
|
|||
this.revokeRefreshToken = revokeRefreshToken;
|
||||
}
|
||||
|
||||
public int getRefreshTokenMaxReuse() {
|
||||
return refreshTokenMaxReuse;
|
||||
}
|
||||
|
||||
public void setRefreshTokenMaxReuse(int revokeRefreshTokenCount) {
|
||||
this.refreshTokenMaxReuse = revokeRefreshTokenCount;
|
||||
}
|
||||
|
||||
public int getSsoSessionIdleTimeout() {
|
||||
return ssoSessionIdleTimeout;
|
||||
}
|
||||
|
|
|
@ -112,4 +112,10 @@
|
|||
baseTableName="RESOURCE_SERVER_SCOPE" baseColumnNames="RESOURCE_SERVER_ID"
|
||||
referencedTableName="RESOURCE_SERVER" referencedColumnNames="ID"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="glavoie@gmail.com" id="authn-3.4.0.CR1-refresh-token-max-reuse">
|
||||
<addColumn tableName="REALM">
|
||||
<column name="REFRESH_TOKEN_MAX_REUSE" type="INT" defaultValueNumeric="0"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -140,6 +140,26 @@ public class PersistentAuthenticatedClientSessionAdapter implements Authenticate
|
|||
model.setTimestamp(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentRefreshToken() {
|
||||
return null; // Information not persisted.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentRefreshToken(String currentRefreshToken) {
|
||||
// Information not persisted.
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentRefreshTokenUseCount() {
|
||||
return 0; // Information not persisted.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount) {
|
||||
// Information not persisted.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAction() {
|
||||
return getData().getAction();
|
||||
|
|
|
@ -255,6 +255,7 @@ public class ModelToRepresentation {
|
|||
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
|
||||
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
|
||||
rep.setRevokeRefreshToken(realm.isRevokeRefreshToken());
|
||||
rep.setRefreshTokenMaxReuse(realm.getRefreshTokenMaxReuse());
|
||||
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
|
||||
rep.setAccessTokenLifespanForImplicitFlow(realm.getAccessTokenLifespanForImplicitFlow());
|
||||
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
||||
|
|
|
@ -165,6 +165,9 @@ public class RepresentationToModel {
|
|||
if (rep.getRevokeRefreshToken() != null) newRealm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
|
||||
else newRealm.setRevokeRefreshToken(false);
|
||||
|
||||
if (rep.getRefreshTokenMaxReuse() != null) newRealm.setRefreshTokenMaxReuse(rep.getRefreshTokenMaxReuse());
|
||||
else newRealm.setRefreshTokenMaxReuse(0);
|
||||
|
||||
if (rep.getAccessTokenLifespan() != null) newRealm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
||||
else newRealm.setAccessTokenLifespan(300);
|
||||
|
||||
|
@ -841,6 +844,7 @@ public class RepresentationToModel {
|
|||
realm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan());
|
||||
if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
|
||||
if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
|
||||
if (rep.getRefreshTokenMaxReuse() != null) realm.setRefreshTokenMaxReuse(rep.getRefreshTokenMaxReuse());
|
||||
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
||||
if (rep.getAccessTokenLifespanForImplicitFlow() != null)
|
||||
realm.setAccessTokenLifespanForImplicitFlow(rep.getAccessTokenLifespanForImplicitFlow());
|
||||
|
|
|
@ -30,6 +30,12 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
|
|||
void setUserSession(UserSessionModel userSession);
|
||||
UserSessionModel getUserSession();
|
||||
|
||||
String getCurrentRefreshToken();
|
||||
void setCurrentRefreshToken(String currentRefreshToken);
|
||||
|
||||
int getCurrentRefreshTokenUseCount();
|
||||
void setCurrentRefreshTokenUseCount(int currentRefreshTokenUseCount);
|
||||
|
||||
String getNote(String name);
|
||||
void setNote(String name, String value);
|
||||
void removeNote(String name);
|
||||
|
|
|
@ -159,6 +159,9 @@ public interface RealmModel extends RoleContainerModel {
|
|||
boolean isRevokeRefreshToken();
|
||||
void setRevokeRefreshToken(boolean revokeRefreshToken);
|
||||
|
||||
int getRefreshTokenMaxReuse();
|
||||
void setRefreshTokenMaxReuse(int revokeRefreshTokenCount);
|
||||
|
||||
int getSsoSessionIdleTimeout();
|
||||
void setSsoSessionIdleTimeout(int seconds);
|
||||
|
||||
|
|
|
@ -250,17 +250,9 @@ public class TokenManager {
|
|||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token. Token client and authorized client don't match");
|
||||
}
|
||||
|
||||
validateTokenReuse(session, realm, refreshToken, validation);
|
||||
|
||||
int currentTime = Time.currentTime();
|
||||
|
||||
if (realm.isRevokeRefreshToken()) {
|
||||
int clusterStartupTime = session.getProvider(ClusterProvider.class).getClusterStartupTime();
|
||||
|
||||
if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() && (clusterStartupTime != validation.clientSession.getTimestamp())) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
validation.clientSession.setTimestamp(currentTime);
|
||||
validation.userSession.setLastSessionRefresh(currentTime);
|
||||
|
||||
|
@ -278,6 +270,36 @@ public class TokenManager {
|
|||
return new RefreshResult(res, TokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType()));
|
||||
}
|
||||
|
||||
private void validateTokenReuse(KeycloakSession session, RealmModel realm, RefreshToken refreshToken,
|
||||
TokenValidation validation) throws OAuthErrorException {
|
||||
if (realm.isRevokeRefreshToken()) {
|
||||
int clusterStartupTime = session.getProvider(ClusterProvider.class).getClusterStartupTime();
|
||||
|
||||
if (validation.clientSession.getCurrentRefreshToken() != null &&
|
||||
!refreshToken.getId().equals(validation.clientSession.getCurrentRefreshToken()) &&
|
||||
refreshToken.getIssuedAt() < validation.clientSession.getTimestamp() &&
|
||||
clusterStartupTime != validation.clientSession.getTimestamp()) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
|
||||
}
|
||||
}
|
||||
|
||||
if (realm.isRevokeRefreshToken()) {
|
||||
if (!refreshToken.getId().equals(validation.clientSession.getCurrentRefreshToken())) {
|
||||
validation.clientSession.setCurrentRefreshToken(refreshToken.getId());
|
||||
validation.clientSession.setCurrentRefreshTokenUseCount(0);
|
||||
}
|
||||
|
||||
int currentCount = validation.clientSession.getCurrentRefreshTokenUseCount();
|
||||
if (currentCount > realm.getRefreshTokenMaxReuse()) {
|
||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Maximum allowed refresh token reuse exceeded",
|
||||
"Maximum allowed refresh token reuse exceeded");
|
||||
}
|
||||
validation.clientSession.setCurrentRefreshTokenUseCount(currentCount + 1);
|
||||
} else {
|
||||
validation.clientSession.setCurrentRefreshToken(null);
|
||||
}
|
||||
}
|
||||
|
||||
public RefreshToken verifyRefreshToken(KeycloakSession session, RealmModel realm, String encodedRefreshToken) throws OAuthErrorException {
|
||||
return verifyRefreshToken(session, realm, encodedRefreshToken, true);
|
||||
}
|
||||
|
|
|
@ -27,11 +27,9 @@ import org.keycloak.events.Details;
|
|||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.util.ClientManager;
|
||||
|
@ -273,6 +271,178 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenReuseTokenWithRefreshTokensRevokedAfterSingleReuse() throws Exception {
|
||||
try {
|
||||
RealmManager.realm(adminClient.realm("test"))
|
||||
.revokeRefreshToken(true)
|
||||
.refreshTokenMaxReuse(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 initialResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
RefreshToken initialRefreshToken = oauth.verifyRefreshToken(initialResponse.getRefreshToken());
|
||||
|
||||
events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||
|
||||
setTimeOffset(2);
|
||||
|
||||
// Initial refresh.
|
||||
OAuthClient.AccessTokenResponse responseFirstUse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken(), "password");
|
||||
RefreshToken newTokenFirstUse = oauth.verifyRefreshToken(responseFirstUse.getRefreshToken());
|
||||
|
||||
assertEquals(200, responseFirstUse.getStatusCode());
|
||||
|
||||
events.expectRefresh(initialRefreshToken.getId(), sessionId).assertEvent();
|
||||
|
||||
setTimeOffset(4);
|
||||
|
||||
// Second refresh (allowed).
|
||||
OAuthClient.AccessTokenResponse responseFirstReuse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken(), "password");
|
||||
RefreshToken newTokenFirstReuse = oauth.verifyRefreshToken(responseFirstReuse.getRefreshToken());
|
||||
|
||||
assertEquals(200, responseFirstReuse.getStatusCode());
|
||||
|
||||
events.expectRefresh(initialRefreshToken.getId(), sessionId).assertEvent();
|
||||
|
||||
// Token reused twice, became invalid.
|
||||
OAuthClient.AccessTokenResponse responseSecondReuse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken(), "password");
|
||||
|
||||
assertEquals(400, responseSecondReuse.getStatusCode());
|
||||
|
||||
events.expectRefresh(initialRefreshToken.getId(), sessionId).removeDetail(Details.TOKEN_ID)
|
||||
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
|
||||
|
||||
// Refresh token from first use became invalid.
|
||||
OAuthClient.AccessTokenResponse responseUseOfInvalidatedRefreshToken =
|
||||
oauth.doRefreshTokenRequest(responseFirstUse.getRefreshToken(), "password");
|
||||
|
||||
assertEquals(400, responseUseOfInvalidatedRefreshToken.getStatusCode());
|
||||
|
||||
events.expectRefresh(newTokenFirstUse.getId(), sessionId).removeDetail(Details.TOKEN_ID)
|
||||
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
|
||||
|
||||
// Refresh token from reuse is still valid.
|
||||
OAuthClient.AccessTokenResponse responseUseOfValidRefreshToken =
|
||||
oauth.doRefreshTokenRequest(responseFirstReuse.getRefreshToken(), "password");
|
||||
|
||||
assertEquals(200, responseUseOfValidRefreshToken.getStatusCode());
|
||||
|
||||
events.expectRefresh(newTokenFirstReuse.getId(), sessionId).assertEvent();
|
||||
} finally {
|
||||
setTimeOffset(0);
|
||||
RealmManager.realm(adminClient.realm("test"))
|
||||
.refreshTokenMaxReuse(0)
|
||||
.revokeRefreshToken(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenReuseOfExistingTokenAfterEnablingReuseRevokation() throws Exception {
|
||||
try {
|
||||
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 initialResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
RefreshToken initialRefreshToken = oauth.verifyRefreshToken(initialResponse.getRefreshToken());
|
||||
|
||||
events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||
|
||||
setTimeOffset(2);
|
||||
|
||||
// Infinite reuse allowed
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
|
||||
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true).refreshTokenMaxReuse(1);
|
||||
|
||||
// Config changed, we start tracking reuse.
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
|
||||
OAuthClient.AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken(), "password");
|
||||
|
||||
assertEquals(400, responseReuseExceeded.getStatusCode());
|
||||
|
||||
events.expectRefresh(initialRefreshToken.getId(), sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
|
||||
} finally {
|
||||
setTimeOffset(0);
|
||||
RealmManager.realm(adminClient.realm("test"))
|
||||
.refreshTokenMaxReuse(0)
|
||||
.revokeRefreshToken(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshTokenReuseOfExistingTokenAfterDisablingReuseRevokation() throws Exception {
|
||||
try {
|
||||
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true).refreshTokenMaxReuse(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 initialResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
RefreshToken initialRefreshToken = oauth.verifyRefreshToken(initialResponse.getRefreshToken());
|
||||
|
||||
events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||
|
||||
setTimeOffset(2);
|
||||
|
||||
// Single reuse authorized.
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
|
||||
OAuthClient.AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken(), "password");
|
||||
|
||||
assertEquals(400, responseReuseExceeded.getStatusCode());
|
||||
|
||||
events.expectRefresh(initialRefreshToken.getId(), sessionId).removeDetail(Details.TOKEN_ID)
|
||||
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
|
||||
|
||||
RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
|
||||
|
||||
// Config changed, token can be reused again
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
|
||||
} finally {
|
||||
setTimeOffset(0);
|
||||
RealmManager.realm(adminClient.realm("test"))
|
||||
.refreshTokenMaxReuse(0)
|
||||
.revokeRefreshToken(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void processExpectedValidRefresh(String sessionId, RefreshToken requestToken, String refreshToken) {
|
||||
OAuthClient.AccessTokenResponse response2 = oauth.doRefreshTokenRequest(refreshToken, "password");
|
||||
RefreshToken refreshToken2 = oauth.verifyRefreshToken(response2.getRefreshToken());
|
||||
|
||||
assertEquals(200, response2.getStatusCode());
|
||||
|
||||
events.expectRefresh(requestToken.getId(), sessionId).assertEvent();
|
||||
}
|
||||
|
||||
|
||||
String privateKey;
|
||||
String publicKey;
|
||||
|
||||
|
|
|
@ -53,6 +53,13 @@ public class RealmManager {
|
|||
return this;
|
||||
}
|
||||
|
||||
public RealmManager refreshTokenMaxReuse(int refreshTokenMaxReuse) {
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
rep.setRefreshTokenMaxReuse(refreshTokenMaxReuse);
|
||||
realm.update(rep);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void generateKeys() {
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
|
||||
|
|
|
@ -93,7 +93,9 @@ user-cache-clear.tooltip=Clears all entries from the user cache (this will clear
|
|||
keys-cache-clear=Keys Cache
|
||||
keys-cache-clear.tooltip=Clears all entries from the cache of external public keys. These are keys of external clients or identity providers. (this wil clear entries for all realms)
|
||||
revoke-refresh-token=Revoke Refresh Token
|
||||
revoke-refresh-token.tooltip=If enabled refresh tokens can only be used once. Otherwise refresh tokens are not revoked when used and can be used multiple times.
|
||||
revoke-refresh-token.tooltip=If enabled a refresh token can only be used up to 'Refresh Token Max Reuse' and is revoked when a different token is used. Otherwise refresh tokens are not revoked when used and can be used multiple times.
|
||||
refresh-token-max-reuse=Refresh Token Max Reuse
|
||||
refresh-token-max-reuse.tooltip=Maximum number of times a refresh token can be reused. When a different token is used, revokation is immediate.
|
||||
sso-session-idle=SSO Session Idle
|
||||
seconds=Seconds
|
||||
minutes=Minutes
|
||||
|
|
|
@ -1088,6 +1088,10 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
}
|
||||
}, true);
|
||||
|
||||
$scope.changeRevokeRefreshToken = function() {
|
||||
|
||||
};
|
||||
|
||||
$scope.save = function() {
|
||||
$scope.realm.accessTokenLifespan = $scope.realm.accessTokenLifespan.toSeconds();
|
||||
$scope.realm.accessTokenLifespanForImplicitFlow = $scope.realm.accessTokenLifespanForImplicitFlow.toSeconds();
|
||||
|
|
|
@ -7,13 +7,25 @@
|
|||
<label class="col-md-2 control-label" for="revokeRefreshToken">{{:: 'revoke-refresh-token' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input ng-model="realm.revokeRefreshToken" name="revokeRefreshToken" id="revokeRefreshToken" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
<input ng-change="" ng-model="realm.revokeRefreshToken" name="revokeRefreshToken" id="revokeRefreshToken" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
|
||||
</div>
|
||||
|
||||
<kc-tooltip>{{:: 'revoke-refresh-token.tooltip' | translate}}
|
||||
</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group" data-ng-show="realm.revokeRefreshToken == true">
|
||||
<label class="col-md-2 control-label" for="refreshTokenMaxReuse">{{:: 'refresh-token-max-reuse' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="number" required min="0" max="31536000" data-ng-model="realm.refreshTokenMaxReuse" id="refreshTokenMaxReuse"
|
||||
name="refreshTokenMaxReuse"/>
|
||||
</div>
|
||||
|
||||
<kc-tooltip>{{:: 'refresh-token-max-reuse.tooltip' | translate}}
|
||||
</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="ssoSessionIdleTimeout">{{:: 'sso-session-idle' | translate}}</label>
|
||||
|
||||
|
|
Loading…
Reference in a new issue