diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java
index d930c6faf3..d571b29e44 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java
@@ -121,6 +121,19 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
return new AuthenticationSessionAdapter(session, this, tabId, authSessionEntity);
}
+ @Override
+ public void removeAuthenticationSessionByTabId(String tabId) {
+ if (entity.getAuthenticationSessions().remove(tabId) != null) {
+ if (entity.getAuthenticationSessions().isEmpty()) {
+ provider.tx.remove(cache, entity.getId());
+ } else {
+ entity.setTimestamp(Time.currentTime());
+
+ update();
+ }
+ }
+ }
+
@Override
public void restartSession(RealmModel realm) {
entity.getAuthenticationSessions().clear();
diff --git a/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java
index 5aff0eda8b..5854a9891b 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java
@@ -58,6 +58,12 @@ public interface RootAuthenticationSessionModel {
*/
AuthenticationSessionModel createAuthenticationSession(ClientModel client);
+ /**
+ * Removes authentication session from root authentication session.
+ * If there's no child authentication session left in the root authentication session, it's removed as well.
+ * @param tabId String
+ */
+ void removeAuthenticationSessionByTabId(String tabId);
/**
* Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm.
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index c6a1423c22..f25c3e208a 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -138,6 +138,12 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
}
+ if (oldToken.getIssuedAt() + 1 < userSession.getStarted()) {
+ logger.debug("Refresh toked issued before the user session started");
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh toked issued before the user session started");
+ }
+
+
ClientModel client = session.getContext().getClient();
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
@@ -245,6 +251,9 @@ public class TokenManager {
if (token.getIssuedAt() < session.users().getNotBeforeOfUser(realm, user)) {
return false;
}
+ if (token.getIssuedAt() + 1 < userSession.getStarted()) {
+ return false;
+ }
return true;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
index 6ccbad3fb7..473e8261bb 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
@@ -120,6 +120,10 @@ public class LogoutEndpoint {
try {
IDToken idToken = tokenManager.verifyIDTokenSignature(session, encodedIdToken);
userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
+
+ if (userSession != null) {
+ checkTokenIssuedAt(idToken, userSession);
+ }
} catch (OAuthErrorException e) {
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_TOKEN);
@@ -198,6 +202,7 @@ public class LogoutEndpoint {
}
if (userSessionModel != null) {
+ checkTokenIssuedAt(token, userSessionModel);
logout(userSessionModel, offline);
}
} catch (OAuthErrorException e) {
@@ -235,4 +240,9 @@ public class LogoutEndpoint {
}
}
+ private void checkTokenIssuedAt(IDToken token, UserSessionModel userSession) throws OAuthErrorException {
+ if (token.getIssuedAt() + 1 < userSession.getStarted()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh toked issued before the user session started");
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
index 40c4d767b5..b8eded5eea 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java
@@ -233,11 +233,13 @@ public class UserInfoEndpoint {
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
UserSessionModel offlineUserSession = null;
if (AuthenticationManager.isSessionValid(realm, userSession)) {
+ checkTokenIssuedAt(token, userSession, event);
event.session(userSession);
return userSession;
} else {
offlineUserSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
+ checkTokenIssuedAt(token, offlineUserSession, event);
event.session(offlineUserSession);
return offlineUserSession;
}
@@ -258,4 +260,10 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
}
+ private void checkTokenIssuedAt(AccessToken token, UserSessionModel userSession, EventBuilder event) throws ErrorResponseException {
+ if (token.getIssuedAt() + 1 < userSession.getStarted()) {
+ event.error(Errors.INVALID_TOKEN);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Stale token", Response.Status.UNAUTHORIZED);
+ }
+ }
}
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 d88a8fead4..1c06ad113b 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -242,7 +242,8 @@ public class AuthenticationManager {
backchannelLogoutAll(session, realm, userSession, logoutAuthSession, uriInfo, headers, logoutBroker);
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
} finally {
- asm.removeAuthenticationSession(realm, logoutAuthSession, false);
+ RootAuthenticationSessionModel rootAuthSession = logoutAuthSession.getParentSession();
+ rootAuthSession.removeAuthenticationSessionByTabId(logoutAuthSession.getTabId());
}
userSession.setState(UserSessionModel.State.LOGGED_OUT);
@@ -707,7 +708,7 @@ public class AuthenticationManager {
}
public static void expireCookie(RealmModel realm, String cookieName, String path, boolean httpOnly, ClientConnection connection) {
- logger.debugv("Expiring cookie: {0} path: {1}", cookieName, path);
+ logger.debugf("Expiring cookie: %s path: %s", cookieName, path);
boolean secureOnly = realm.getSslRequired().isRequired(connection);;
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OIDCLogin.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OIDCLogin.java
index bd2130179c..4ce8f126f7 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OIDCLogin.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OIDCLogin.java
@@ -17,6 +17,8 @@
package org.keycloak.testsuite.auth.page.login;
+import org.keycloak.testsuite.util.DroneUtils;
+
/**
*
* @author tkyjovsk
@@ -27,4 +29,14 @@ public class OIDCLogin extends Login {
setProtocol(OIDC);
}
+ @Override
+ public boolean isCurrent() {
+ String realm = "test";
+ return isCurrent(realm);
+ }
+
+ public boolean isCurrent(String realm) {
+ return DroneUtils.getCurrentDriver().getTitle().equals("Log in to " + realm) || DroneUtils.getCurrentDriver().getTitle().equals("Anmeldung bei " + realm);
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/AbstractPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/AbstractPage.java
index d452f294b2..9cdb90ddf6 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/AbstractPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/AbstractPage.java
@@ -18,7 +18,9 @@
package org.keycloak.testsuite.page;
import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.logging.Logger;
+import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.URLUtils;
import org.openqa.selenium.WebDriver;
@@ -105,7 +107,7 @@ public abstract class AbstractPage {
public void assertCurrent() {
String name = getClass().getSimpleName();
- Assert.assertTrue("Expected " + name + " but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
+ Assert.assertTrue("Expected " + name + " but was " + DroneUtils.getCurrentDriver().getTitle() + " (" + DroneUtils.getCurrentDriver().getCurrentUrl() + ")",
isCurrent());
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AbstractPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AbstractPage.java
index 8642568e9f..9a0eb94204 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AbstractPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/AbstractPage.java
@@ -21,6 +21,7 @@ import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.Assert;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.testsuite.arquillian.SuiteContext;
+import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
@@ -44,7 +45,7 @@ public abstract class AbstractPage {
public void assertCurrent() {
String name = getClass().getSimpleName();
- Assert.assertTrue("Expected " + name + " but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")",
+ Assert.assertTrue("Expected " + name + " but was " + DroneUtils.getCurrentDriver().getTitle() + " (" + DroneUtils.getCurrentDriver().getCurrentUrl() + ")",
isCurrent());
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
index e1e75def59..41ff437d6c 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LanguageComboboxAwarePage.java
@@ -17,6 +17,7 @@
package org.keycloak.testsuite.pages;
+import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
@@ -40,7 +41,7 @@ public abstract class LanguageComboboxAwarePage extends AbstractPage {
public void openLanguage(String language){
WebElement langLink = localeDropdown.findElement(By.xpath("//a[text()='" + language + "']"));
String url = langLink.getAttribute("href");
- driver.navigate().to(url);
+ DroneUtils.getCurrentDriver().navigate().to(url);
WaitUtils.waitForPageToLoad();
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
index bd40883797..b83b8ff948 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java
@@ -18,6 +18,7 @@
package org.keycloak.testsuite.pages;
import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
@@ -145,7 +146,7 @@ public class LoginPage extends LanguageComboboxAwarePage {
}
public boolean isCurrent(String realm) {
- return driver.getTitle().equals("Log in to " + realm) || driver.getTitle().equals("Anmeldung bei " + realm);
+ return DroneUtils.getCurrentDriver().getTitle().equals("Log in to " + realm) || DroneUtils.getCurrentDriver().getTitle().equals("Anmeldung bei " + realm);
}
public void clickRegister() {
@@ -159,7 +160,7 @@ public class LoginPage extends LanguageComboboxAwarePage {
public WebElement findSocialButton(String providerId) {
String id = "zocial-" + providerId;
- return this.driver.findElement(By.id(id));
+ return DroneUtils.getCurrentDriver().findElement(By.id(id));
}
public void resetPassword() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
index 33afbafed7..a725620f60 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java
@@ -16,12 +16,14 @@
*/
package org.keycloak.testsuite.forms;
+import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.common.util.Retry;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@@ -40,24 +42,32 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.console.page.AdminConsole;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
+import org.keycloak.testsuite.util.DroneUtils;
+import org.keycloak.testsuite.util.JavascriptBrowser;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.testsuite.util.UserBuilder;
-import org.openqa.selenium.Cookie;
import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebDriver;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
@@ -68,6 +78,7 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
/**
* @author Stian Thorgersen
@@ -94,9 +105,19 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
.build();
user2Id = user2.getId();
+ UserRepresentation admin = UserBuilder.create()
+ .username("admin")
+ .password("admin")
+ .enabled(true)
+ .build();
+ HashMap> clientRoles = new HashMap<>();
+ clientRoles.put("realm-management", Arrays.asList("realm-admin"));
+ admin.setClientRoles(clientRoles);
+
RealmBuilder.edit(testRealm)
.user(user)
- .user(user2);
+ .user(user2)
+ .user(admin);
}
@Rule
@@ -105,9 +126,21 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
@Page
protected AppPage appPage;
+ @Page
+ @JavascriptBrowser
+ protected AdminConsole jsAdminConsole;
+
+ @Drone
+ @JavascriptBrowser
+ protected WebDriver jsDriver;
+
@Page
protected LoginPage loginPage;
+ @Page
+ @JavascriptBrowser
+ protected LoginPage jsLoginPage;
+
@Page
protected ErrorPage errorPage;
@@ -696,6 +729,35 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
.assertEvent();
}
+ @Test
+ public void loginAfterExpiredTimeout() throws Exception {
+ try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm("test"))
+ .updateWith(r -> {
+ r.setSsoSessionMaxLifespan(5);
+ })
+ .update()) {
+
+ DroneUtils.addWebDriver(jsDriver);
+
+ jsAdminConsole.setAdminRealm(testRealm().toRepresentation().getRealm());
+
+ jsAdminConsole.navigateTo();
+ assertCurrentUrlStartsWithLoginUrlOf(jsAdminConsole);
+
+ // login for the first time
+ jsLoginPage.login("admin", "admin");
+
+ // wait for a timeout
+ TimeUnit.SECONDS.sleep(5);
+ Retry.execute(() -> jsLoginPage.assertCurrent(), 20, 500);
+
+ // try to re-login immediately, it should be successful i.e without "You took too long to login. Login process starting from beginning." message
+ jsLoginPage.login("admin", "admin");
+
+ assertFalse(jsLoginPage.isCurrent());
+ }
+ }
+
@Test
public void loginExpiredCodeAndExpiredCookies() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
index a93715b2c2..3cd162a3d1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java
@@ -25,16 +25,19 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
-import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.util.*;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.UriBuilder;
+
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
@@ -107,6 +110,47 @@ public class LogoutTest extends AbstractKeycloakTest {
}
}
+ @Test
+ public void postLogoutWithRefreshTokenAfterUserSessionLogoutAndLoginAgain() throws Exception {
+ // Login
+ OAuthClient.AccessTokenResponse accessTokenResponse = loginAndForceNewLoginPage();
+ String refreshToken1 = accessTokenResponse.getRefreshToken();
+
+ oauth.doLogout(refreshToken1, "password");
+
+ setTimeOffset(2);
+
+ oauth.fillLoginForm("test-user@localhost", "password");
+
+ Assert.assertFalse(loginPage.isCurrent());
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
+
+ // POST logout with token should fail
+ try (CloseableHttpResponse response = oauth.doLogout(refreshToken1, "password")) {
+ assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusLine().getStatusCode());
+ }
+
+ String logoutUrl = oauth.getLogoutUrl()
+ .idTokenHint(accessTokenResponse.getIdToken())
+ .postLogoutRedirectUri(oauth.APP_AUTH_ROOT)
+ .build();
+
+ // GET logout with ID token should fail as well
+ try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
+ CloseableHttpResponse response = c.execute(new HttpGet(logoutUrl))) {
+ assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatusLine().getStatusCode());
+ }
+
+ // finally POST logout with VALID token should succeed
+ try (CloseableHttpResponse response = oauth.doLogout(tokenResponse2.getRefreshToken(), "password")) {
+ assertThat(response, Matchers.statusCodeIsHC(Status.NO_CONTENT));
+
+ assertNotNull(testingClient.testApp().getAdminLogoutAction());
+ }
+ }
+
@Test
public void postLogoutFailWithCredentialsOfDifferentClient() throws Exception {
@@ -248,4 +292,23 @@ public class LogoutTest extends AbstractKeycloakTest {
}
}
+ private OAuthClient.AccessTokenResponse loginAndForceNewLoginPage() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ oauth.clientSessionState("client-session");
+
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+ setTimeOffset(1);
+
+ String loginFormUri = UriBuilder.fromUri(oauth.getLoginFormUrl())
+ .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
+ .build().toString();
+ driver.navigate().to(loginFormUri);
+
+ loginPage.assertCurrent();
+
+ return tokenResponse;
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index 6a202297cd..20d0663a55 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -16,12 +16,14 @@
*/
package org.keycloak.testsuite.oauth;
+import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Details;
@@ -29,6 +31,7 @@ import org.keycloak.events.Errors;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.utils.SessionTimeoutHelper;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
@@ -37,6 +40,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
@@ -63,6 +67,7 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
@@ -74,6 +79,9 @@ import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
*/
public class RefreshTokenTest extends AbstractKeycloakTest {
+ @Page
+ protected LoginPage loginPage;
+
@Rule
public AssertEvents events = new AssertEvents(this);
@@ -470,7 +478,6 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
private void processExpectedValidRefresh(String sessionId, RefreshToken requestToken, String refreshToken) {
OAuthClient.AccessTokenResponse response2 = oauth.doRefreshTokenRequest(refreshToken, "password");
- RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
assertEquals(200, response2.getStatusCode());
@@ -540,6 +547,93 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
events.clear();
}
+ @Test
+ public void refreshTokenAfterUserLogoutAndLoginAgain() {
+ String refreshToken1 = loginAndForceNewLoginPage();
+
+ oauth.doLogout(refreshToken1, "password");
+ events.clear();
+
+ // Set time offset to 2 (Just to simulate to be more close to real situation)
+ setTimeOffset(2);
+
+ // Continue with login
+ oauth.fillLoginForm("test-user@localhost", "password");
+
+ assertFalse(loginPage.isCurrent());
+
+ OAuthClient.AccessTokenResponse tokenResponse2 = null;
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
+
+ // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
+ OAuthClient.AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1, "password");
+ assertEquals(400, responseReuseExceeded.getStatusCode());
+
+ // Finally try with valid refresh token
+ responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken(), "password");
+ assertEquals(200, responseReuseExceeded.getStatusCode());
+ }
+
+ @Test
+ public void refreshTokenAfterAdminLogoutAllAndLoginAgain() {
+ String refreshToken1 = loginAndForceNewLoginPage();
+
+ adminClient.realm("test").logoutAll();
+
+ events.clear();
+
+ // Set time offset to 2 (Just to simulate to be more close to real situation)
+ setTimeOffset(2);
+
+ // Continue with login
+ oauth.fillLoginForm("test-user@localhost", "password");
+
+ assertFalse(loginPage.isCurrent());
+
+ OAuthClient.AccessTokenResponse tokenResponse2 = null;
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
+
+ // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
+ OAuthClient.AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1, "password");
+ assertEquals(400, responseReuseExceeded.getStatusCode());
+
+ // Finally try with valid refresh token
+ responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken(), "password");
+ assertEquals(200, responseReuseExceeded.getStatusCode());
+ }
+
+ @Test
+ public void refreshTokenAfterUserAdminLogoutEndpointAndLoginAgain() {
+ String refreshToken1 = loginAndForceNewLoginPage();
+
+ RefreshToken refreshTokenParsed1 = oauth.parseRefreshToken(refreshToken1);
+ String userId = refreshTokenParsed1.getSubject();
+ UserResource user = adminClient.realm("test").users().get(userId);
+ user.logout();
+
+ // Set time offset to 2 (Just to simulate to be more close to real situation)
+ setTimeOffset(2);
+
+ // Continue with login
+ oauth.fillLoginForm("test-user@localhost", "password");
+
+ assertFalse(loginPage.isCurrent());
+
+ OAuthClient.AccessTokenResponse tokenResponse2 = null;
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
+
+ // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
+ OAuthClient.AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1, "password");
+ assertEquals(400, responseReuseExceeded.getStatusCode());
+
+ // Finally try with valid refresh token
+ responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken(), "password");
+ assertEquals(200, responseReuseExceeded.getStatusCode());
+ }
+
@Test
public void testUserSessionRefreshAndIdle() throws Exception {
oauth.doLogin("test-user@localhost", "password");
@@ -1009,4 +1103,34 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
setTimeOffset(0);
}
+ private String loginAndForceNewLoginPage() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+ events.poll();
+
+ // Assert refresh successful
+ String refreshToken = tokenResponse.getRefreshToken();
+ RefreshToken refreshTokenParsed1 = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
+ processExpectedValidRefresh(sessionId, refreshTokenParsed1, refreshToken);
+
+ // Set time offset to 1 (Just to simulate to be more close to real situation)
+ setTimeOffset(1);
+
+ // Open the tab with prompt=login. AuthenticationSession will be created with same ID like userSession
+ String loginFormUri = UriBuilder.fromUri(oauth.getLoginFormUrl())
+ .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
+ .build().toString();
+ driver.navigate().to(loginFormUri);
+
+ loginPage.assertCurrent();
+
+ return refreshToken;
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
index 42cd9c1d92..6851e9e0dd 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
@@ -26,6 +26,8 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.events.Errors;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
@@ -40,10 +42,12 @@ import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.oidc.OIDCScopeTest;
import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
import org.keycloak.testsuite.util.KeycloakModelUtils;
+import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.util.JsonSerialization;
+import javax.ws.rs.core.UriBuilder;
import java.util.ArrayList;
import java.util.List;
@@ -179,6 +183,36 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
assertEquals(jsonNode.get("typ").asText(), "Refresh");
}
+ @Test
+ public void testIntrospectRefreshTokenAfterUserSessionLogoutAndLoginAgain() throws Exception {
+ AccessTokenResponse accessTokenResponse = loginAndForceNewLoginPage();
+ String refreshToken1 = accessTokenResponse.getRefreshToken();
+
+ oauth.doLogout(refreshToken1, "password");
+ events.clear();
+
+ setTimeOffset(2);
+
+ oauth.fillLoginForm("test-user@localhost", "password");
+ events.expectLogin().assertEvent();
+
+ Assert.assertFalse(loginPage.isCurrent());
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ OAuthClient.AccessTokenResponse tokenResponse2 = oauth.doAccessTokenRequest(code, "password");
+
+ String introspectResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", tokenResponse2.getRefreshToken());
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode jsonNode = objectMapper.readTree(introspectResponse);
+ assertTrue(jsonNode.get("active").asBoolean());
+
+ introspectResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", refreshToken1);
+
+ jsonNode = objectMapper.readTree(introspectResponse);
+ assertFalse(jsonNode.get("active").asBoolean());
+ }
+
@Test
public void testPublicClientCredentialsNotAllowed() throws Exception {
oauth.doLogin("test-user@localhost", "password");
@@ -389,4 +423,24 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
assertEquals(Errors.INVALID_CLIENT, rep.getOtherClaims().get("error"));
assertNull(rep.getSubject());
}
+
+ private OAuthClient.AccessTokenResponse loginAndForceNewLoginPage() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ oauth.clientSessionState("client-session");
+
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+ setTimeOffset(1);
+
+ String loginFormUri = UriBuilder.fromUri(oauth.getLoginFormUrl())
+ .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
+ .build().toString();
+ driver.navigate().to(loginFormUri);
+
+ loginPage.assertCurrent();
+
+ return tokenResponse;
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
index 0281196e66..42c2bf0fa4 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java
@@ -32,6 +32,7 @@ import org.keycloak.events.EventType;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.testsuite.util.KeycloakModelUtils;
@@ -342,6 +343,44 @@ public class UserInfoTest extends AbstractKeycloakTest {
}
}
+ @Test
+ public void testAccessTokenAfterUserSessionLogoutAndLoginAgain() {
+ OAuthClient.AccessTokenResponse accessTokenResponse = loginAndForceNewLoginPage();
+ String refreshToken1 = accessTokenResponse.getRefreshToken();
+
+ oauth.doLogout(refreshToken1, "password");
+ events.clear();
+
+ setTimeOffset(2);
+
+ oauth.fillLoginForm("test-user@localhost", "password");
+ events.expectLogin().assertEvent();
+
+ Assert.assertFalse(loginPage.isCurrent());
+
+ events.clear();
+
+ Client client = ClientBuilder.newClient();
+
+ try {
+ Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getAccessToken());
+
+ assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+
+ response.close();
+
+ events.expect(EventType.USER_INFO_REQUEST_ERROR)
+ .error(Errors.INVALID_TOKEN)
+ .user(Matchers.nullValue(String.class))
+ .session(Matchers.nullValue(String.class))
+ .detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
+ .client("test-app")
+ .assertEvent();
+ } finally {
+ client.close();
+ }
+ }
+
@Test
public void testSessionExpiredOfflineAccess() throws Exception {
Client client = ClientBuilder.newClient();
@@ -518,4 +557,23 @@ public class UserInfoTest extends AbstractKeycloakTest {
}
}
+ private OAuthClient.AccessTokenResponse loginAndForceNewLoginPage() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ oauth.clientSessionState("client-session");
+
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
+
+ setTimeOffset(1);
+
+ String loginFormUri = UriBuilder.fromUri(oauth.getLoginFormUrl())
+ .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
+ .build().toString();
+ driver.navigate().to(loginFormUri);
+
+ loginPage.assertCurrent();
+
+ return tokenResponse;
+ }
}