commit
96099e0798
17 changed files with 213 additions and 4 deletions
|
@ -48,5 +48,11 @@
|
||||||
|
|
||||||
<addPrimaryKey columnNames="USER_SESSION_ID, OFFLINE" constraintName="CONSTRAINT_OFFLINE_US_SES_PK" tableName="OFFLINE_USER_SESSION"/>
|
<addPrimaryKey columnNames="USER_SESSION_ID, OFFLINE" constraintName="CONSTRAINT_OFFLINE_US_SES_PK" tableName="OFFLINE_USER_SESSION"/>
|
||||||
<addPrimaryKey columnNames="CLIENT_SESSION_ID, OFFLINE" constraintName="CONSTRAINT_OFFLINE_CL_SES_PK" tableName="OFFLINE_CLIENT_SESSION"/>
|
<addPrimaryKey columnNames="CLIENT_SESSION_ID, OFFLINE" constraintName="CONSTRAINT_OFFLINE_CL_SES_PK" tableName="OFFLINE_CLIENT_SESSION"/>
|
||||||
|
|
||||||
|
<addColumn tableName="REALM">
|
||||||
|
<column name="REVOKE_REFRESH_TOKEN" type="BOOLEAN" defaultValueBoolean="false">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
|
@ -10,6 +10,7 @@ public class RealmRepresentation {
|
||||||
protected String id;
|
protected String id;
|
||||||
protected String realm;
|
protected String realm;
|
||||||
protected Integer notBefore;
|
protected Integer notBefore;
|
||||||
|
protected Boolean revokeRefreshToken;
|
||||||
protected Integer accessTokenLifespan;
|
protected Integer accessTokenLifespan;
|
||||||
protected Integer ssoSessionIdleTimeout;
|
protected Integer ssoSessionIdleTimeout;
|
||||||
protected Integer ssoSessionMaxLifespan;
|
protected Integer ssoSessionMaxLifespan;
|
||||||
|
@ -166,6 +167,14 @@ public class RealmRepresentation {
|
||||||
this.sslRequired = sslRequired;
|
this.sslRequired = sslRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getRevokeRefreshToken() {
|
||||||
|
return revokeRefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRevokeRefreshToken(Boolean revokeRefreshToken) {
|
||||||
|
this.revokeRefreshToken = revokeRefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getAccessTokenLifespan() {
|
public Integer getAccessTokenLifespan() {
|
||||||
return accessTokenLifespan;
|
return accessTokenLifespan;
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,19 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<title>Version specific migration</title>
|
<title>Version specific migration</title>
|
||||||
|
<section>
|
||||||
|
<title>Migrating to 1.6.0.Final</title>
|
||||||
|
<simplesect>
|
||||||
|
<title>Refresh tokens are not reusable anymore</title>
|
||||||
|
<para>
|
||||||
|
Old versions of Keycloak allowed reusing refresh tokens multiple times. Keycloak no longer permits
|
||||||
|
this by default. When a refresh token is used to obtain a new access token a new refresh token is also
|
||||||
|
included. This new refresh token should be used next time the access token is refreshed. If this is
|
||||||
|
a problem for you it's possible to enable reuse of refresh tokens in the admin console under token
|
||||||
|
settings.
|
||||||
|
</para>
|
||||||
|
</simplesect>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<title>Migrating to 1.5.0.Final</title>
|
<title>Migrating to 1.5.0.Final</title>
|
||||||
<simplesect>
|
<simplesect>
|
||||||
|
|
|
@ -66,6 +66,8 @@ realm-cache-enabled=Realm Cache Enabled
|
||||||
realm-cache-enabled.tooltip=Enable/disable cache for realm, client and role data.
|
realm-cache-enabled.tooltip=Enable/disable cache for realm, client and role data.
|
||||||
user-cache-enabled=User Cache Enabled
|
user-cache-enabled=User Cache Enabled
|
||||||
user-cache-enabled.tooltip=Enable/disable user and user role mapping cache.
|
user-cache-enabled.tooltip=Enable/disable user and user role mapping cache.
|
||||||
|
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.
|
||||||
sso-session-idle=SSO Session Idle
|
sso-session-idle=SSO Session Idle
|
||||||
seconds=Seconds
|
seconds=Seconds
|
||||||
minutes=Minutes
|
minutes=Minutes
|
||||||
|
|
|
@ -3,6 +3,17 @@
|
||||||
|
|
||||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
|
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<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}}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<kc-tooltip>{{:: 'revoke-refresh-token.tooltip' | translate}}
|
||||||
|
</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-2 control-label" for="ssoSessionIdleTimeout">{{:: 'sso-session-idle' | translate}}</label>
|
<label class="col-md-2 control-label" for="ssoSessionIdleTimeout">{{:: 'sso-session-idle' | translate}}</label>
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,9 @@ public interface RealmModel extends RoleContainerModel {
|
||||||
|
|
||||||
void setResetPasswordAllowed(boolean resetPasswordAllowed);
|
void setResetPasswordAllowed(boolean resetPasswordAllowed);
|
||||||
|
|
||||||
|
boolean isRevokeRefreshToken();
|
||||||
|
void setRevokeRefreshToken(boolean revokeRefreshToken);
|
||||||
|
|
||||||
int getSsoSessionIdleTimeout();
|
int getSsoSessionIdleTimeout();
|
||||||
void setSsoSessionIdleTimeout(int seconds);
|
void setSsoSessionIdleTimeout(int seconds);
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
|
||||||
private int failureFactor;
|
private int failureFactor;
|
||||||
//--- end brute force settings
|
//--- end brute force settings
|
||||||
|
|
||||||
|
private boolean revokeRefreshToken;
|
||||||
private int ssoSessionIdleTimeout;
|
private int ssoSessionIdleTimeout;
|
||||||
private int ssoSessionMaxLifespan;
|
private int ssoSessionMaxLifespan;
|
||||||
private int accessTokenLifespan;
|
private int accessTokenLifespan;
|
||||||
|
@ -229,6 +230,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
|
||||||
this.failureFactor = failureFactor;
|
this.failureFactor = failureFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRevokeRefreshToken() {
|
||||||
|
return revokeRefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRevokeRefreshToken(boolean revokeRefreshToken) {
|
||||||
|
this.revokeRefreshToken = revokeRefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
public int getSsoSessionIdleTimeout() {
|
public int getSsoSessionIdleTimeout() {
|
||||||
return ssoSessionIdleTimeout;
|
return ssoSessionIdleTimeout;
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,6 +144,7 @@ public class ModelToRepresentation {
|
||||||
rep.setVerifyEmail(realm.isVerifyEmail());
|
rep.setVerifyEmail(realm.isVerifyEmail());
|
||||||
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
|
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
|
||||||
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
|
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
|
||||||
|
rep.setRevokeRefreshToken(realm.isRevokeRefreshToken());
|
||||||
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
|
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
|
||||||
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
|
||||||
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
|
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package org.keycloak.models.utils;
|
package org.keycloak.models.utils;
|
||||||
|
|
||||||
import org.keycloak.models.session.PersistentClientSessionModel;
|
|
||||||
import org.keycloak.models.session.PersistentUserSessionModel;
|
|
||||||
import org.keycloak.representations.idm.OfflineClientSessionRepresentation;
|
|
||||||
import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
|
|
||||||
import org.keycloak.util.Base64;
|
import org.keycloak.util.Base64;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.enums.SslRequired;
|
import org.keycloak.enums.SslRequired;
|
||||||
|
@ -100,6 +96,9 @@ public class RepresentationToModel {
|
||||||
|
|
||||||
if (rep.getNotBefore() != null) newRealm.setNotBefore(rep.getNotBefore());
|
if (rep.getNotBefore() != null) newRealm.setNotBefore(rep.getNotBefore());
|
||||||
|
|
||||||
|
if (rep.getRevokeRefreshToken() != null) newRealm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
|
||||||
|
else newRealm.setRevokeRefreshToken(false);
|
||||||
|
|
||||||
if (rep.getAccessTokenLifespan() != null) newRealm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
if (rep.getAccessTokenLifespan() != null) newRealm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
||||||
else newRealm.setAccessTokenLifespan(300);
|
else newRealm.setAccessTokenLifespan(300);
|
||||||
|
|
||||||
|
@ -532,6 +531,7 @@ public class RepresentationToModel {
|
||||||
if (rep.getAccessCodeLifespanUserAction() != null) realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
|
if (rep.getAccessCodeLifespanUserAction() != null) realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
|
||||||
if (rep.getAccessCodeLifespanLogin() != null) realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin());
|
if (rep.getAccessCodeLifespanLogin() != null) realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin());
|
||||||
if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
|
if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
|
||||||
|
if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
|
||||||
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
||||||
if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
|
if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
|
||||||
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
|
||||||
|
|
|
@ -325,6 +325,16 @@ public class RealmAdapter implements RealmModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRevokeRefreshToken() {
|
||||||
|
return realm.isRevokeRefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRevokeRefreshToken(boolean revokeRefreshToken) {
|
||||||
|
realm.setRevokeRefreshToken(revokeRefreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSsoSessionIdleTimeout() {
|
public int getSsoSessionIdleTimeout() {
|
||||||
return realm.getSsoSessionIdleTimeout();
|
return realm.getSsoSessionIdleTimeout();
|
||||||
|
|
|
@ -239,6 +239,18 @@ public class RealmAdapter implements RealmModel {
|
||||||
updated.setEditUsernameAllowed(editUsernameAllowed);
|
updated.setEditUsernameAllowed(editUsernameAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRevokeRefreshToken() {
|
||||||
|
if (updated != null) return updated.isRevokeRefreshToken();
|
||||||
|
return cached.isRevokeRefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRevokeRefreshToken(boolean revokeRefreshToken) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.setRevokeRefreshToken(revokeRefreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSsoSessionIdleTimeout() {
|
public int getSsoSessionIdleTimeout() {
|
||||||
if (updated != null) return updated.getSsoSessionIdleTimeout();
|
if (updated != null) return updated.getSsoSessionIdleTimeout();
|
||||||
|
|
|
@ -55,6 +55,7 @@ public class CachedRealm implements Serializable {
|
||||||
private int failureFactor;
|
private int failureFactor;
|
||||||
//--- end brute force settings
|
//--- end brute force settings
|
||||||
|
|
||||||
|
private boolean revokeRefreshToken;
|
||||||
private int ssoSessionIdleTimeout;
|
private int ssoSessionIdleTimeout;
|
||||||
private int ssoSessionMaxLifespan;
|
private int ssoSessionMaxLifespan;
|
||||||
private int accessTokenLifespan;
|
private int accessTokenLifespan;
|
||||||
|
@ -136,6 +137,7 @@ public class CachedRealm implements Serializable {
|
||||||
failureFactor = model.getFailureFactor();
|
failureFactor = model.getFailureFactor();
|
||||||
//--- end brute force settings
|
//--- end brute force settings
|
||||||
|
|
||||||
|
revokeRefreshToken = model.isRevokeRefreshToken();
|
||||||
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
|
||||||
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
|
||||||
accessTokenLifespan = model.getAccessTokenLifespan();
|
accessTokenLifespan = model.getAccessTokenLifespan();
|
||||||
|
@ -313,6 +315,10 @@ public class CachedRealm implements Serializable {
|
||||||
return editUsernameAllowed;
|
return editUsernameAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRevokeRefreshToken() {
|
||||||
|
return revokeRefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
public int getSsoSessionIdleTimeout() {
|
public int getSsoSessionIdleTimeout() {
|
||||||
return ssoSessionIdleTimeout;
|
return ssoSessionIdleTimeout;
|
||||||
}
|
}
|
||||||
|
|
|
@ -336,6 +336,16 @@ public class RealmAdapter implements RealmModel {
|
||||||
realm.setNotBefore(notBefore);
|
realm.setNotBefore(notBefore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRevokeRefreshToken() {
|
||||||
|
return realm.isRevokeRefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRevokeRefreshToken(boolean revokeRefreshToken) {
|
||||||
|
realm.setRevokeRefreshToken(revokeRefreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAccessTokenLifespan() {
|
public int getAccessTokenLifespan() {
|
||||||
return realm.getAccessTokenLifespan();
|
return realm.getAccessTokenLifespan();
|
||||||
|
|
|
@ -76,6 +76,8 @@ public class RealmEntity {
|
||||||
@Column(name="EDIT_USERNAME_ALLOWED")
|
@Column(name="EDIT_USERNAME_ALLOWED")
|
||||||
protected boolean editUsernameAllowed;
|
protected boolean editUsernameAllowed;
|
||||||
|
|
||||||
|
@Column(name="REVOKE_REFRESH_TOKEN")
|
||||||
|
private boolean revokeRefreshToken;
|
||||||
@Column(name="SSO_IDLE_TIMEOUT")
|
@Column(name="SSO_IDLE_TIMEOUT")
|
||||||
private int ssoSessionIdleTimeout;
|
private int ssoSessionIdleTimeout;
|
||||||
@Column(name="SSO_MAX_LIFESPAN")
|
@Column(name="SSO_MAX_LIFESPAN")
|
||||||
|
@ -288,6 +290,14 @@ public class RealmEntity {
|
||||||
this.editUsernameAllowed = editUsernameAllowed;
|
this.editUsernameAllowed = editUsernameAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isRevokeRefreshToken() {
|
||||||
|
return revokeRefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRevokeRefreshToken(boolean revokeRefreshToken) {
|
||||||
|
this.revokeRefreshToken = revokeRefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
public int getSsoSessionIdleTimeout() {
|
public int getSsoSessionIdleTimeout() {
|
||||||
return ssoSessionIdleTimeout;
|
return ssoSessionIdleTimeout;
|
||||||
}
|
}
|
||||||
|
|
|
@ -311,6 +311,16 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
||||||
updateRealm();
|
updateRealm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRevokeRefreshToken() {
|
||||||
|
return realm.isRevokeRefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRevokeRefreshToken(boolean revokeRefreshToken) {
|
||||||
|
realm.setRevokeRefreshToken(revokeRefreshToken);
|
||||||
|
updateRealm();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSsoSessionIdleTimeout() {
|
public int getSsoSessionIdleTimeout() {
|
||||||
|
|
|
@ -161,6 +161,15 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
int currentTime = Time.currentTime();
|
int currentTime = Time.currentTime();
|
||||||
|
|
||||||
|
if (realm.isRevokeRefreshToken() && !refreshToken.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE)) {
|
||||||
|
if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp()) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
|
||||||
|
}
|
||||||
|
|
||||||
|
validation.clientSession.setTimestamp(currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
validation.userSession.setLastSessionRefresh(currentTime);
|
validation.userSession.setLastSessionRefresh(currentTime);
|
||||||
|
|
||||||
AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
|
AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.enums.SslRequired;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.Event;
|
import org.keycloak.events.Event;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
@ -181,6 +182,93 @@ public class RefreshTokenTest {
|
||||||
Time.setOffset(0);
|
Time.setOffset(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenReuseTokenWithoutRefreshTokensRevoked() throws Exception {
|
||||||
|
try {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
Event loginEvent = events.expectLogin().assertEvent();
|
||||||
|
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
RefreshToken refreshToken1 = oauth.verifyRefreshToken(response1.getRefreshToken());
|
||||||
|
|
||||||
|
events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||||
|
|
||||||
|
Time.setOffset(2);
|
||||||
|
|
||||||
|
AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
|
||||||
|
Assert.assertEquals(200, response2.getStatusCode());
|
||||||
|
|
||||||
|
events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
|
||||||
|
|
||||||
|
AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
|
||||||
|
|
||||||
|
Assert.assertEquals(200, response3.getStatusCode());
|
||||||
|
|
||||||
|
events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
|
||||||
|
} finally {
|
||||||
|
Time.setOffset(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenReuseTokenWithRefreshTokensRevoked() throws Exception {
|
||||||
|
try {
|
||||||
|
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
appRealm.setRevokeRefreshToken(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
Event loginEvent = events.expectLogin().assertEvent();
|
||||||
|
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
RefreshToken refreshToken1 = oauth.verifyRefreshToken(response1.getRefreshToken());
|
||||||
|
|
||||||
|
events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||||
|
|
||||||
|
Time.setOffset(2);
|
||||||
|
|
||||||
|
AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
|
||||||
|
RefreshToken refreshToken2 = oauth.verifyRefreshToken(response2.getRefreshToken());
|
||||||
|
|
||||||
|
Assert.assertEquals(200, response2.getStatusCode());
|
||||||
|
|
||||||
|
events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
|
||||||
|
|
||||||
|
AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
|
||||||
|
|
||||||
|
Assert.assertEquals(400, response3.getStatusCode());
|
||||||
|
|
||||||
|
events.expectRefresh(refreshToken1.getId(), sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
|
||||||
|
|
||||||
|
oauth.doRefreshTokenRequest(response2.getRefreshToken(), "password");
|
||||||
|
|
||||||
|
events.expectRefresh(refreshToken2.getId(), sessionId).assertEvent();
|
||||||
|
} finally {
|
||||||
|
Time.setOffset(0);
|
||||||
|
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
appRealm.setRevokeRefreshToken(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PrivateKey privateKey;
|
PrivateKey privateKey;
|
||||||
PublicKey publicKey;
|
PublicKey publicKey;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue