Details
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 7a567b3..453f550 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -5,6 +5,7 @@ import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
+import org.keycloak.audit.Audit;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.ClientModel;
@@ -62,10 +63,28 @@ public class AuthenticationManager {
}
public static boolean isSessionValid(RealmModel realm, UserSessionModel session) {
+ if (session == null) return false;
int currentTime = Time.currentTime();
- return session == null || session.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() < currentTime || session.getStarted() + realm.getSsoSessionMaxLifespan() < currentTime;
+ int max = session.getStarted() + realm.getSsoSessionMaxLifespan();
+ boolean valid = session != null && session.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() > currentTime && max > currentTime;
+ return valid;
}
+ public static void logout(RealmModel realm, UserSessionModel session, UriInfo uriInfo) {
+ if (session == null) return;
+ UserModel user = session.getUser();
+
+ logger.infov("Logging out: {0} ({1})", user.getLoginName(), session.getId());
+
+ realm.removeUserSession(session);
+ expireIdentityCookie(realm, uriInfo);
+ expireRememberMeCookie(realm, uriInfo);
+
+ new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), session.getId());
+
+ }
+
+
public AccessToken createIdentityToken(RealmModel realm, UserModel user, UserSessionModel session) {
logger.info("createIdentityToken");
AccessToken token = new AccessToken();
@@ -121,26 +140,26 @@ public class AuthenticationManager {
return encodedToken;
}
- public void expireIdentityCookie(RealmModel realm, UriInfo uriInfo) {
+ public static void expireIdentityCookie(RealmModel realm, UriInfo uriInfo) {
logger.debug("Expiring identity cookie");
String path = getIdentityCookiePath(realm, uriInfo);
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, path, true);
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, path, false);
expireRememberMeCookie(realm, uriInfo);
}
- public void expireRememberMeCookie(RealmModel realm, UriInfo uriInfo) {
+ public static void expireRememberMeCookie(RealmModel realm, UriInfo uriInfo) {
logger.debug("Expiring remember me cookie");
String path = getIdentityCookiePath(realm, uriInfo);
String cookieName = KEYCLOAK_REMEMBER_ME;
expireCookie(realm, cookieName, path, true);
}
- protected String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) {
+ protected static String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) {
URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName());
return uri.getRawPath();
}
- public void expireCookie(RealmModel realm, String cookieName, String path, boolean httpOnly) {
+ public static void expireCookie(RealmModel realm, String cookieName, String path, boolean httpOnly) {
logger.debugv("Expiring cookie: {0} path: {1}", cookieName, path);
boolean secureOnly = !realm.isSslNotRequired();
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly);
@@ -196,11 +215,8 @@ public class AuthenticationManager {
}
UserSessionModel session = realm.getUserSession(token.getSessionState());
- int currentTime = Time.currentTime();
- if (isSessionValid(realm, session)) {
- if (session != null) {
- realm.removeUserSession(session);
- }
+ if (!isSessionValid(realm, session)) {
+ if (session != null) logout(realm, session, uriInfo);
logger.info("User session not active");
return null;
}
diff --git a/services/src/main/java/org/keycloak/services/managers/TokenManager.java b/services/src/main/java/org/keycloak/services/managers/TokenManager.java
index 94f134f..89adfaf 100755
--- a/services/src/main/java/org/keycloak/services/managers/TokenManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/TokenManager.java
@@ -23,6 +23,7 @@ import org.keycloak.representations.RefreshToken;
import org.keycloak.util.Time;
import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashSet;
@@ -106,7 +107,7 @@ public class TokenManager {
return code;
}
- public AccessToken refreshAccessToken(RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException {
+ public AccessToken refreshAccessToken(UriInfo uriInfo, RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException {
JWSInput jws = new JWSInput(encodedRefreshToken);
RefreshToken refreshToken = null;
try {
@@ -138,10 +139,8 @@ public class TokenManager {
UserSessionModel session = realm.getUserSession(refreshToken.getSessionState());
int currentTime = Time.currentTime();
- if (AuthenticationManager.isSessionValid(realm, session)) {
- if (session != null) {
- realm.removeUserSession(session);
- }
+ if (!AuthenticationManager.isSessionValid(realm, session)) {
+ AuthenticationManager.logout(realm, session, uriInfo);
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
}
diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
index 17e257c..4c6139c 100755
--- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
@@ -408,7 +408,8 @@ public class RequiredActionsService {
AuthenticationManager authManager = new AuthenticationManager(providerSession);
UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
- if (AuthenticationManager.isSessionValid(realm, session)) {
+ if (!AuthenticationManager.isSessionValid(realm, session)) {
+ AuthenticationManager.logout(realm, session, uriInfo);
return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri());
}
audit.session(session);
diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java
index 2746461..e9c2c80 100755
--- a/services/src/main/java/org/keycloak/services/resources/TokenService.java
+++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java
@@ -296,13 +296,14 @@ public class TokenService {
String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN);
AccessToken accessToken = null;
try {
- accessToken = tokenManager.refreshAccessToken(realm, client, refreshToken, audit);
+ accessToken = tokenManager.refreshAccessToken(uriInfo, realm, client, refreshToken, audit);
} catch (OAuthErrorException e) {
Map<String, String> error = new HashMap<String, String>();
error.put(OAuth2Constants.ERROR, e.getError());
if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription());
audit.error(Errors.INVALID_TOKEN);
- throw new BadRequestException("OAuth Error", e, Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build());
+ logger.error("OAuth Error", e);
+ return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
}
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
@@ -635,7 +636,8 @@ public class TokenService {
}
UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
- if (AuthenticationManager.isSessionValid(realm, session)) {
+ if (!AuthenticationManager.isSessionValid(realm, session)) {
+ AuthenticationManager.logout(realm, session, uriInfo);
Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant");
res.put(OAuth2Constants.ERROR_DESCRIPTION, "Session not active");
@@ -854,15 +856,7 @@ public class TokenService {
private void logout(UserSessionModel session) {
UserModel user = session.getUser();
-
- logger.infov("Logging out: {0} ({1})", user.getLoginName(), session.getId());
-
- realm.removeUserSession(session);
- authManager.expireIdentityCookie(realm, uriInfo);
- authManager.expireRememberMeCookie(realm, uriInfo);
-
- resourceAdminManager.logoutUser(uriInfo.getRequestUri(), realm, user.getId(), session.getId());
-
+ authManager.logout(realm, session, uriInfo);
audit.user(user).session(session).success();
}
@@ -914,8 +908,8 @@ public class TokenService {
}
UserSessionModel session = realm.getUserSession(accessCodeEntry.getSessionState());
- int currentTime = Time.currentTime();
- if (AuthenticationManager.isSessionValid(realm, session)) {
+ if (!AuthenticationManager.isSessionValid(realm, session)) {
+ AuthenticationManager.logout(realm, session, uriInfo);
audit.error(Errors.INVALID_CODE);
return oauth.forwardToSecurityFailure("Session not active");
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
index 46c7bb2..ba5aaab 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
@@ -28,6 +28,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@@ -156,4 +157,67 @@ public class AdapterTest {
}
+
+ @Test
+ public void testLoginSSOIdle() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.getRealmByName("demo");
+ int originalIdle = realm.getSsoSessionIdleTimeout();
+ realm.setSsoSessionIdleTimeout(1);
+ keycloakRule.stopSession(session, true);
+
+ Thread.sleep(2000);
+
+
+ // test SSO
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+ session = keycloakRule.startSession();
+ realm = session.getRealmByName("demo");
+ realm.setSsoSessionIdleTimeout(originalIdle);
+ keycloakRule.stopSession(session, true);
+ }
+ @Test
+ public void testLoginSSOMax() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.getRealmByName("demo");
+ int original = realm.getSsoSessionMaxLifespan();
+ realm.setSsoSessionMaxLifespan(1);
+ keycloakRule.stopSession(session, true);
+
+ Thread.sleep(2000);
+
+
+ // test SSO
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+ session = keycloakRule.startSession();
+ realm = session.getRealmByName("demo");
+ realm.setSsoSessionMaxLifespan(original);
+ keycloakRule.stopSession(session, true);
+ }
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index b661790..cead905 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -29,8 +29,14 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details;
import org.keycloak.audit.Errors;
import org.keycloak.audit.Event;
+import org.keycloak.models.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
@@ -41,9 +47,7 @@ import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.util.Time;
import org.openqa.selenium.WebDriver;
-import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.greaterThanOrEqualTo;
-import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -162,4 +166,131 @@ public class RefreshTokenTest {
events.clear();
}
+ @Test
+ public void testUserSessionRefreshAndIdle() throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+ events.poll();
+
+ String refreshId = oauth.verifyRefreshToken(tokenResponse.getRefreshToken()).getId();
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.getRealmByName("test");
+ UserSessionModel userSession = realm.getUserSession(sessionId);
+ int last = userSession.getLastSessionRefresh();
+ keycloakRule.stopSession(session, false);
+
+ Thread.sleep(2000);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
+
+ AccessToken refreshedToken = oauth.verifyToken(tokenResponse.getAccessToken());
+ RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(tokenResponse.getRefreshToken());
+
+ Assert.assertEquals(200, tokenResponse.getStatusCode());
+
+ session = keycloakRule.startSession();
+ realm = session.getRealmByName("test");
+ userSession = realm.getUserSession(sessionId);
+ int next = userSession.getLastSessionRefresh();
+ keycloakRule.stopSession(session, false);
+
+ // should not update last refresh because the access token interval is way less than idle timeout
+ Assert.assertEquals(last, next);
+
+
+
+ session = keycloakRule.startSession();
+ realm = session.getRealmByName("test");
+ int lastAccessTokenLifespan = realm.getAccessTokenLifespan();
+ realm.setAccessTokenLifespan(100000);
+ keycloakRule.stopSession(session, true);
+
+ Thread.sleep(2000);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
+
+ session = keycloakRule.startSession();
+ realm = session.getRealmByName("test");
+ userSession = realm.getUserSession(sessionId);
+ next = userSession.getLastSessionRefresh();
+ keycloakRule.stopSession(session, false);
+
+ // lastSEssionRefresh should be updated because access code lifespan is higher than sso idle timeout
+ Assert.assertThat(next, allOf(greaterThan(last), lessThan(last + 6)));
+
+ session = keycloakRule.startSession();
+ realm = session.getRealmByName("test");
+ int originalIdle = realm.getSsoSessionIdleTimeout();
+ realm.setSsoSessionIdleTimeout(1);
+ keycloakRule.stopSession(session, true);
+
+ events.clear();
+ Thread.sleep(2000);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
+
+ // test idle timeout
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
+
+ session = keycloakRule.startSession();
+ realm = session.getRealmByName("test");
+ realm.setSsoSessionIdleTimeout(originalIdle);
+ realm.setAccessTokenLifespan(lastAccessTokenLifespan);
+ keycloakRule.stopSession(session, true);
+
+ events.clear();
+ }
+
+ @Test
+ public void refreshTokenUserSessionMaxLifespan() throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+ events.poll();
+
+ String refreshId = oauth.verifyRefreshToken(tokenResponse.getRefreshToken()).getId();
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.getRealmByName("test");
+ int maxLifespan = realm.getSsoSessionMaxLifespan();
+ realm.setSsoSessionMaxLifespan(1);
+ keycloakRule.stopSession(session, true);
+
+ Thread.sleep(1000);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
+
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ session = keycloakRule.startSession();
+ realm = session.getRealmByName("test");
+ realm.setSsoSessionMaxLifespan(maxLifespan);
+ keycloakRule.stopSession(session, true);
+
+
+ events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
+
+ events.clear();
+ }
+
+
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
index f0e37cc..9312eee 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
@@ -133,4 +133,17 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
byte[] bytes = os.toByteArray();
return JsonSerialization.readValue(bytes, RealmRepresentation.class);
}
+
+ public KeycloakSession startSession() {
+ KeycloakSession session = server.getKeycloakSessionFactory().createSession();
+ session.getTransaction().begin();
+ return session;
+ }
+
+ public void stopSession(KeycloakSession session, boolean commit) {
+ if (commit) {
+ session.getTransaction().commit();
+ }
+ session.close();
+ }
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
index d04cc21..4096658 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
@@ -81,19 +81,6 @@ public class KeycloakRule extends AbstractKeycloakRule {
}
}
- public KeycloakSession startSession() {
- KeycloakSession session = server.getKeycloakSessionFactory().createSession();
- session.getTransaction().begin();
- return session;
- }
-
- public void stopSession(KeycloakSession session, boolean commit) {
- if (commit) {
- session.getTransaction().commit();
- }
- session.close();
- }
-
public void removeUserSession(String sessionId) {
KeycloakSession keycloakSession = startSession();
RealmModel realm = keycloakSession.getRealm("test");