KEYCLOAK-4215 Consider session expiration when setting token timeouts

This commit is contained in:
stianst 2017-12-06 13:58:14 +01:00 committed by Stian Thorgersen
parent 5fd3c9161d
commit c055ffb083
8 changed files with 96 additions and 9 deletions

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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");

View file

@ -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)));
}
}

View file

@ -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);