From 67e3e60f28585bd2871d13d1fda31bf585edcb07 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 15 May 2014 23:10:14 -0400 Subject: [PATCH] test sso idle, logout on idle --- .../managers/AuthenticationManager.java | 36 +++-- .../services/managers/TokenManager.java | 9 +- .../resources/RequiredActionsService.java | 3 +- .../services/resources/TokenService.java | 22 +-- .../testsuite/adapter/AdapterTest.java | 64 ++++++++ .../testsuite/oauth/RefreshTokenTest.java | 137 +++++++++++++++++- .../testsuite/rule/AbstractKeycloakRule.java | 13 ++ .../keycloak/testsuite/rule/KeycloakRule.java | 13 -- 8 files changed, 251 insertions(+), 46 deletions(-) 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 7a567b3756..453f550a40 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 94f134fc33..89adfaf964 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 17e257c501..4c6139cad1 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 2746461e72..e9c2c80ab7 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 error = new HashMap(); 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 res = new HashMap(); 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 46c7bb25b6..ba5aaab132 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 b661790fb2..cead905f48 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 f0e37cc164..9312eeeb4e 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 d04cc21e05..4096658210 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");