KEYCLOAK-4215 Consider session expiration when setting token timeouts
This commit is contained in:
parent
5fd3c9161d
commit
c055ffb083
8 changed files with 96 additions and 9 deletions
|
@ -224,6 +224,11 @@ public class UserSessionAdapter implements UserSessionModel {
|
|||
update(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return offline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNote(String name) {
|
||||
return entity.getNotes() != null ? entity.getNotes().get(name) : null;
|
||||
|
|
|
@ -252,6 +252,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
|||
model.setUserSessionId(entity.getUserSessionId());
|
||||
model.setLastSessionRefresh(entity.getLastSessionRefresh());
|
||||
model.setData(entity.getData());
|
||||
model.setOffline(offlineFromString(entity.getOffline()));
|
||||
|
||||
Map<String, AuthenticatedClientSessionModel> clientSessions = new HashMap<>();
|
||||
return new PersistentUserSessionAdapter(model, realm, user, clientSessions);
|
||||
|
@ -287,4 +288,8 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
|||
private String offlineToString(boolean offline) {
|
||||
return offline ? "1" : "0";
|
||||
}
|
||||
|
||||
private boolean offlineFromString(String offlineStr) {
|
||||
return "1".equals(offlineStr);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,6 +156,11 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
|
|||
model.setLastSessionRefresh(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return model.isOffline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions() {
|
||||
return authenticatedClientSessions;
|
||||
|
|
|
@ -24,7 +24,7 @@ public class PersistentUserSessionModel {
|
|||
|
||||
private String userSessionId;
|
||||
private int lastSessionRefresh;
|
||||
|
||||
private boolean offline;
|
||||
private String data;
|
||||
|
||||
public String getUserSessionId() {
|
||||
|
@ -43,6 +43,13 @@ public class PersistentUserSessionModel {
|
|||
this.lastSessionRefresh = lastSessionRefresh;
|
||||
}
|
||||
|
||||
public boolean isOffline() {
|
||||
return offline;
|
||||
}
|
||||
|
||||
public void setOffline(boolean offline) {
|
||||
this.offline = offline;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
|
|
|
@ -53,6 +53,8 @@ public interface UserSessionModel {
|
|||
|
||||
void setLastSessionRefresh(int seconds);
|
||||
|
||||
boolean isOffline();
|
||||
|
||||
/**
|
||||
* Returns map where key is ID of the client (its UUID) and value is ID respective {@link AuthenticatedClientSessionModel} object.
|
||||
* @return
|
||||
|
|
|
@ -635,11 +635,8 @@ public class TokenManager {
|
|||
|
||||
|
||||
token.setSessionState(session.getId());
|
||||
token.expiration(getTokenExpiration(realm, session, clientSession));
|
||||
|
||||
int tokenLifespan = getTokenLifespan(realm, clientSession);
|
||||
if (tokenLifespan > 0) {
|
||||
token.expiration(Time.currentTime() + tokenLifespan);
|
||||
}
|
||||
Set<String> allowedOrigins = client.getWebOrigins();
|
||||
if (allowedOrigins != null) {
|
||||
token.setAllowedOrigins(WebOriginsUtils.resolveValidWebOrigins(uriInfo, client));
|
||||
|
@ -647,13 +644,22 @@ public class TokenManager {
|
|||
return token;
|
||||
}
|
||||
|
||||
private int getTokenLifespan(RealmModel realm, AuthenticatedClientSessionModel clientSession) {
|
||||
private int getTokenExpiration(RealmModel realm, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
||||
boolean implicitFlow = false;
|
||||
String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||
if (responseType != null) {
|
||||
implicitFlow = OIDCResponseType.parse(responseType).isImplicitFlow();
|
||||
}
|
||||
return implicitFlow ? realm.getAccessTokenLifespanForImplicitFlow() : realm.getAccessTokenLifespan();
|
||||
int tokenLifespan = implicitFlow ? realm.getAccessTokenLifespanForImplicitFlow() : realm.getAccessTokenLifespan();
|
||||
|
||||
int expiration = Time.currentTime() + tokenLifespan;
|
||||
|
||||
if (!userSession.isOffline()) {
|
||||
int sessionExpires = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
|
||||
expiration = expiration <= sessionExpires ? expiration : sessionExpires;
|
||||
}
|
||||
|
||||
return expiration;
|
||||
}
|
||||
|
||||
protected void addComposites(AccessToken token, RoleModel role) {
|
||||
|
@ -765,13 +771,19 @@ public class TokenManager {
|
|||
sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
|
||||
} else {
|
||||
refreshToken = new RefreshToken(accessToken);
|
||||
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
|
||||
refreshToken.expiration(getRefreshExpiration());
|
||||
}
|
||||
refreshToken.id(KeycloakModelUtils.generateId());
|
||||
refreshToken.issuedNow();
|
||||
return this;
|
||||
}
|
||||
|
||||
private int getRefreshExpiration() {
|
||||
int sessionExpires = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
|
||||
int expiration = Time.currentTime() + realm.getSsoSessionIdleTimeout();
|
||||
return expiration <= sessionExpires ? expiration : sessionExpires;
|
||||
}
|
||||
|
||||
public AccessTokenResponseBuilder generateIDToken() {
|
||||
if (accessToken == null) {
|
||||
throw new IllegalStateException("accessToken not set");
|
||||
|
|
|
@ -34,6 +34,10 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
|
@ -133,4 +137,9 @@ public class Assert extends org.junit.Assert {
|
|||
Assert.assertEquals(helpText, property.getHelpText());
|
||||
Assert.assertEquals(type, property.getType());
|
||||
}
|
||||
|
||||
public static void assertExpiration(int actual, int expected) {
|
||||
org.junit.Assert.assertThat(actual, allOf(greaterThanOrEqualTo(expected - 50), lessThanOrEqualTo(expected)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.keycloak.admin.client.resource.ClientTemplateResource;
|
|||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
|
@ -80,6 +81,7 @@ import java.io.IOException;
|
|||
import java.net.URI;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
|
@ -94,8 +96,8 @@ import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
|
|||
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
|
||||
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
|
||||
import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper;
|
||||
import static org.keycloak.testsuite.Assert.assertExpiration;
|
||||
|
||||
import org.keycloak.util.TokenUtil;
|
||||
import org.openqa.selenium.By;
|
||||
|
||||
/**
|
||||
|
@ -974,6 +976,46 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
// KEYCLOAK-4215
|
||||
@Test
|
||||
public void expiration() throws Exception {
|
||||
int sessionMax = (int) TimeUnit.MINUTES.toSeconds(30);
|
||||
int sessionIdle = (int) TimeUnit.MINUTES.toSeconds(30);
|
||||
int tokenLifespan = (int) TimeUnit.MINUTES.toSeconds(5);
|
||||
|
||||
RealmResource realm = adminClient.realm("test");
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
Integer originalSessionMax = rep.getSsoSessionMaxLifespan();
|
||||
rep.setSsoSessionMaxLifespan(sessionMax);
|
||||
realm.update(rep);
|
||||
|
||||
try {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
// Assert refresh expiration equals session idle
|
||||
assertExpiration(response.getRefreshExpiresIn(), sessionIdle);
|
||||
|
||||
// Assert token expiration equals token lifespan
|
||||
assertExpiration(response.getExpiresIn(), tokenLifespan);
|
||||
|
||||
setTimeOffset(sessionMax - 60);
|
||||
|
||||
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
// Assert expiration equals session expiration
|
||||
assertExpiration(response.getRefreshExpiresIn(), 60);
|
||||
assertExpiration(response.getExpiresIn(), 60);
|
||||
} finally {
|
||||
rep.setSsoSessionMaxLifespan(originalSessionMax);
|
||||
realm.update(rep);
|
||||
}
|
||||
}
|
||||
|
||||
private IDToken getIdToken(org.keycloak.representations.AccessTokenResponse tokenResponse) throws JWSInputException {
|
||||
JWSInput input = new JWSInput(tokenResponse.getIdToken());
|
||||
return input.readJsonContent(IDToken.class);
|
||||
|
|
Loading…
Reference in a new issue