From 7516d8035f33f696158abdb0e2354f7a2bc1735a Mon Sep 17 00:00:00 2001
From: Marek Posolda
Date: Tue, 10 Mar 2026 08:57:49 +0100
Subject: [PATCH] Migrate RefreshTokenTest to new testsuite (#46886)
closes #46612
Signed-off-by: mposolda
---
.../realm/RealmConfigBuilder.java | 40 +
test-framework/remote-providers/pom.xml | 5 +
.../timeoffset}/InfinispanTimeUtil.java | 4 +-
.../TimeOffSetRealmResourceProvider.java | 17 +-
.../remote/runonserver/RunOnServerClient.java | 15 +-
.../remote/timeoffset/InjectTimeOffSet.java | 7 +
.../remote/timeoffset/TimeOffSet.java | 14 +-
.../remote/timeoffset/TimeOffsetSupplier.java | 3 +-
.../ui/page/AbstractLoginPage.java | 13 +
.../testframework/ui/page/LoginPage.java | 11 +
.../ui/webdriver/BrowserTabUtils.java | 106 +
.../ui/webdriver/DriverUtils.java | 10 +-
.../ui/webdriver/ManagedWebDriver.java | 14 +-
.../tests/common/TestRealmUserConfig.java | 18 +
.../AuthenticationSessionProviderTest.java | 2 +-
.../tests/model/UserSessionProviderTest.java | 2 +-
.../tests/oauth/RefreshTokenRevokeTest.java | 586 ++++
.../tests/oauth/RefreshTokenTest.java | 1185 +++++++++
.../tests/oauth/RefreshTokenTimeoutsTest.java | 881 ++++++
.../tests/suites/ClusterlessTestSuite.java | 9 +-
.../tests/suites/DatabaseTestSuite.java | 2 +
.../tests/suites/MultisiteTestSuite.java | 9 +-
.../suites/VolatileSessionsTestSuite.java | 9 +-
.../util/oauth/AbstractHttpResponse.java | 4 +
.../util/oauth/UserInfoResponse.java | 18 +
.../OAuthProofKeyForCodeExchangeTest.java | 6 +-
.../testsuite/oauth/RefreshTokenTest.java | 2350 -----------------
.../keycloak/testsuite/oauth/hok/HoKTest.java | 8 +-
.../tests/base/testsuites/clusterless-suite | 1 -
.../tests/base/testsuites/database-suite | 1 -
.../base/testsuites/volatile-sessions-suite | 1 -
31 files changed, 2976 insertions(+), 2375 deletions(-)
rename {tests/utils/src/main/java/org/keycloak/tests/utils/infinispan => test-framework/remote-providers/src/main/java/org/keycloak/testframework/remote/providers/timeoffset}/InfinispanTimeUtil.java (91%)
create mode 100644 test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/BrowserTabUtils.java
create mode 100644 tests/base/src/test/java/org/keycloak/tests/common/TestRealmUserConfig.java
create mode 100644 tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenRevokeTest.java
create mode 100755 tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTest.java
create mode 100644 tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTimeoutsTest.java
delete mode 100755 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java
index 7662413bcbd..6f3d725bac2 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java
@@ -229,6 +229,46 @@ public class RealmConfigBuilder {
return this;
}
+ public RealmConfigBuilder revokeRefreshToken(boolean enabled) {
+ rep.setRevokeRefreshToken(enabled);
+ return this;
+ }
+
+ public RealmConfigBuilder refreshTokenMaxReuse(Integer refreshTokenMaxReuse) {
+ rep.setRefreshTokenMaxReuse(refreshTokenMaxReuse);
+ return this;
+ }
+
+ public RealmConfigBuilder ssoSessionIdleTimeout(Integer ssoSessionIdleTimeout) {
+ rep.setSsoSessionIdleTimeout(ssoSessionIdleTimeout);
+ return this;
+ }
+
+ public RealmConfigBuilder ssoSessionIdleTimeoutRememberMe(Integer ssoSessionIdleTimeoutRememberMe) {
+ rep.setSsoSessionIdleTimeoutRememberMe(ssoSessionIdleTimeoutRememberMe);
+ return this;
+ }
+
+ public RealmConfigBuilder ssoSessionMaxLifespan(Integer ssoSessionMaxLifespan) {
+ rep.setSsoSessionMaxLifespan(ssoSessionMaxLifespan);
+ return this;
+ }
+
+ public RealmConfigBuilder ssoSessionMaxLifespanRememberMe(Integer ssoSessionMaxLifespanRememberMe) {
+ rep.setSsoSessionMaxLifespanRememberMe(ssoSessionMaxLifespanRememberMe);
+ return this;
+ }
+
+ public RealmConfigBuilder clientSessionMaxLifespan(Integer clientSessionMaxLifespan) {
+ rep.setClientSessionMaxLifespan(clientSessionMaxLifespan);
+ return this;
+ }
+
+ public RealmConfigBuilder clientSessionIdleTimeout(Integer clientSessionIdleTimeout) {
+ rep.setClientSessionIdleTimeout(clientSessionIdleTimeout);
+ return this;
+ }
+
public RealmConfigBuilder bruteForceProtected(boolean enabled) {
rep.setBruteForceProtected(enabled);
return this;
diff --git a/test-framework/remote-providers/pom.xml b/test-framework/remote-providers/pom.xml
index 9868f5f8019..7b21cea0291 100644
--- a/test-framework/remote-providers/pom.xml
+++ b/test-framework/remote-providers/pom.xml
@@ -48,6 +48,11 @@
keycloak-server-spi-private
provided
+
+ org.keycloak
+ keycloak-model-infinispan
+ provided
+
jakarta.ws.rs
jakarta.ws.rs-api
diff --git a/tests/utils/src/main/java/org/keycloak/tests/utils/infinispan/InfinispanTimeUtil.java b/test-framework/remote-providers/src/main/java/org/keycloak/testframework/remote/providers/timeoffset/InfinispanTimeUtil.java
similarity index 91%
rename from tests/utils/src/main/java/org/keycloak/tests/utils/infinispan/InfinispanTimeUtil.java
rename to test-framework/remote-providers/src/main/java/org/keycloak/testframework/remote/providers/timeoffset/InfinispanTimeUtil.java
index 59f9c7210b5..4f084e068c6 100644
--- a/tests/utils/src/main/java/org/keycloak/tests/utils/infinispan/InfinispanTimeUtil.java
+++ b/test-framework/remote-providers/src/main/java/org/keycloak/testframework/remote/providers/timeoffset/InfinispanTimeUtil.java
@@ -1,4 +1,4 @@
-package org.keycloak.tests.utils.infinispan;
+package org.keycloak.testframework.remote.providers.timeoffset;
import java.io.Serializable;
@@ -30,7 +30,7 @@ public class InfinispanTimeUtil implements Serializable {
public static void enableTestingTimeService(KeycloakSession session) {
if (origTimeService != null) {
- throw new IllegalStateException("Calling setTestingTimeService when testing TimeService was already set");
+ return;
}
InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
diff --git a/test-framework/remote-providers/src/main/java/org/keycloak/testframework/remote/providers/timeoffset/TimeOffSetRealmResourceProvider.java b/test-framework/remote-providers/src/main/java/org/keycloak/testframework/remote/providers/timeoffset/TimeOffSetRealmResourceProvider.java
index 84570c22a91..ba20edd6e71 100644
--- a/test-framework/remote-providers/src/main/java/org/keycloak/testframework/remote/providers/timeoffset/TimeOffSetRealmResourceProvider.java
+++ b/test-framework/remote-providers/src/main/java/org/keycloak/testframework/remote/providers/timeoffset/TimeOffSetRealmResourceProvider.java
@@ -18,6 +18,7 @@ public class TimeOffSetRealmResourceProvider implements RealmResourceProvider {
private final KeycloakSession session;
private final String KEY_OFFSET = "offset";
+ private final String CACHES = "caches";
public TimeOffSetRealmResourceProvider(KeycloakSession session) {
this.session = session;
@@ -46,11 +47,23 @@ public class TimeOffSetRealmResourceProvider implements RealmResourceProvider {
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
- public Response setTimeOffset(Map time) {
+ public Response setTimeOffset(Map time) {
if (!time.containsKey(KEY_OFFSET)) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
- Time.setOffset(time.get(KEY_OFFSET));
+
+ int timeOffset = (Integer) time.get(KEY_OFFSET);
+ Time.setOffset(timeOffset);
+
+ boolean caches = time.containsKey(CACHES) ? (Boolean) time.get(CACHES) : false;
+ if (caches) {
+ if (timeOffset > 0) {
+ InfinispanTimeUtil.enableTestingTimeService(session);
+ } else {
+ InfinispanTimeUtil.disableTestingTimeService(session);
+ }
+ }
+
return Response.ok().header("Content-Type", MediaType.APPLICATION_JSON).build();
}
}
diff --git a/test-framework/remote/src/main/java/org/keycloak/testframework/remote/runonserver/RunOnServerClient.java b/test-framework/remote/src/main/java/org/keycloak/testframework/remote/runonserver/RunOnServerClient.java
index b76ec1eaafb..ac3f3e42d05 100644
--- a/test-framework/remote/src/main/java/org/keycloak/testframework/remote/runonserver/RunOnServerClient.java
+++ b/test-framework/remote/src/main/java/org/keycloak/testframework/remote/runonserver/RunOnServerClient.java
@@ -43,6 +43,17 @@ public class RunOnServerClient {
return fetch(wrapper.getRunOnServer(), wrapper.getResultClass());
}
+ /**
+ * Retrieve String value from the server. It returns decoded string value (exactly same value returned by the remote method on the server side)
+ *
+ * @param function the function to execute
+ * @return decoded string value (exactly same value returned by the remote method on the server side)
+ * @throws RunOnServerException
+ */
+ public String fetchString(FetchOnServer function) throws RunOnServerException {
+ return fetch(function, String.class);
+ }
+
/**
* Retrieve some value from the Keycloak server using the specified function
* @param function the function to execute
@@ -53,7 +64,7 @@ public class RunOnServerClient {
*/
public T fetch(FetchOnServer function, Class clazz) throws RunOnServerException {
try {
- String s = fetchString(function);
+ String s = fetchStringInternal(function);
return s == null ? null : JsonSerialization.readValue(s, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
@@ -66,7 +77,7 @@ public class RunOnServerClient {
* @return the value
* @throws RunOnServerException
*/
- public String fetchString(FetchOnServer function) throws RunOnServerException {
+ private String fetchStringInternal(FetchOnServer function) throws RunOnServerException {
String encoded = SerializationUtil.encode(function);
String result = runOnServer(encoded);
diff --git a/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/InjectTimeOffSet.java b/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/InjectTimeOffSet.java
index 9cadb90a81d..3a1c1e042c5 100644
--- a/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/InjectTimeOffSet.java
+++ b/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/InjectTimeOffSet.java
@@ -16,5 +16,12 @@ public @interface InjectTimeOffSet {
LifeCycle lifecycle() default LifeCycle.METHOD;
+ /**
+ * Specifies whether time-offset should be integrated with underlying caches (EG. infinispan)
+ *
+ * @return
+ */
+ boolean enableForCaches() default false;
+
int offset() default 0;
}
diff --git a/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/TimeOffSet.java b/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/TimeOffSet.java
index 52cdeefba8d..f2ca9dcb185 100644
--- a/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/TimeOffSet.java
+++ b/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/TimeOffSet.java
@@ -19,19 +19,29 @@ import org.apache.http.entity.StringEntity;
public class TimeOffSet {
private int currentOffset;
private final String KEY_OFFSET = "offset";
+ private final String CACHES = "caches";
private final String TIME_OFFSET_ENDPOINT = "/testing-timeoffset";
private final HttpClient httpClient;
private final String serverUrl;
+ private boolean enableForCaches;
- public TimeOffSet(HttpClient httpClient, String serverUrl, int initOffset) {
+ public TimeOffSet(HttpClient httpClient, String serverUrl, int initOffset, boolean enableForCaches) {
this.httpClient = httpClient;
this.serverUrl = serverUrl;
+ this.enableForCaches = enableForCaches;
if (initOffset != 0) {
set(initOffset);
}
currentOffset = initOffset;
}
+ public void enableForCaches() {
+ this.enableForCaches = true;
+ if (currentOffset != 0) {
+ set(currentOffset); // Refresh the server (in case that timeOffset was already set there)
+ }
+ }
+
/**
* Set the timeoffset on the Keycloak server
*
@@ -45,7 +55,7 @@ public class TimeOffSet {
Time.setOffset(currentOffset);
// set for KC server
- var time = Map.of(KEY_OFFSET, currentOffset);
+ var time = Map.of(KEY_OFFSET, currentOffset, CACHES, enableForCaches);
try {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(time);
diff --git a/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/TimeOffsetSupplier.java b/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/TimeOffsetSupplier.java
index a0cdc1afb61..3561a8f89b0 100644
--- a/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/TimeOffsetSupplier.java
+++ b/test-framework/remote/src/main/java/org/keycloak/testframework/remote/timeoffset/TimeOffsetSupplier.java
@@ -29,7 +29,8 @@ public class TimeOffsetSupplier implements SupplierNote: For one particular WebDriver has to exist only one BrowserTabUtil instance. (Right order of tabs)
+ *
+ * @author Martin Bartos
+ */
+public class BrowserTabUtils {
+
+ private final ManagedWebDriver managedDriver;
+ private WebDriver driver;
+ private JavascriptExecutor jsExecutor;
+ private List tabs;
+
+ BrowserTabUtils(ManagedWebDriver managedDriver) {
+ this.managedDriver = managedDriver;
+ driverValidation();
+ }
+
+ private void driverValidation() {
+ this.driver = managedDriver.driver();
+ this.jsExecutor = (JavascriptExecutor) driver;
+ tabs = new ArrayList<>(driver.getWindowHandles());
+ }
+
+
+ public String getActualWindowHandle() {
+ return driver.getWindowHandle();
+ }
+
+ public void switchToTab(String windowHandle) {
+ driver.switchTo().window(windowHandle);
+ }
+
+ public void switchToTab(int index) {
+ assertValidIndex(index);
+ switchToTab(tabs.get(index));
+ }
+
+ public void newTab(String url) {
+ jsExecutor.executeScript("window.open(arguments[0]);", url);
+
+ final Set handles = driver.getWindowHandles();
+ final String tabHandle = handles.stream()
+ .filter(tab -> !tabs.contains(tab))
+ .findFirst()
+ .orElse(null);
+
+ if (handles.size() > tabs.size() + 1) {
+ throw new RuntimeException("Too many window handles. You can only create a new one by this method.");
+ }
+
+ if (tabHandle == null) {
+ throw new RuntimeException("Creating the new tab failed.");
+ }
+
+ tabs.add(tabHandle);
+ switchToTab(tabHandle);
+ }
+
+ public void closeTab(int index) {
+ assertValidIndex(index);
+
+ if (index == 0 || getCountOfTabs() == 1)
+ throw new RuntimeException("You must not close the original tab.");
+
+ switchToTab(index);
+ driver.close();
+
+ tabs.remove(index);
+ switchToTab(index - 1);
+ }
+
+ public int getCountOfTabs() {
+ return tabs.size();
+ }
+
+ /**
+ * Close all browser tabs with the exception of the single original tab (tab with index 0), which should be always kept opened
+ */
+ public void closeTabs() {
+ for (int i = 1; i < getCountOfTabs(); i++) {
+ closeTab(i);
+ }
+ }
+
+ private boolean validIndex(int index) {
+ return (index >= 0 && tabs != null && index < tabs.size());
+ }
+
+ private void assertValidIndex(int index) {
+ if (!validIndex(index))
+ throw new IndexOutOfBoundsException("Invalid index of tab.");
+ }
+
+}
diff --git a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/DriverUtils.java b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/DriverUtils.java
index 1edfa585fd3..0e9746c31d6 100644
--- a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/DriverUtils.java
+++ b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/DriverUtils.java
@@ -4,6 +4,7 @@ import java.io.File;
import org.keycloak.testframework.config.Config;
+import org.htmlunit.WebClientOptions;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.firefox.FirefoxDriver;
@@ -39,7 +40,14 @@ class DriverUtils {
static HtmlUnitDriver createHtmlUnitDriver() {
HtmlUnitDriver driver = new HtmlUnitDriver(DriverOptions.createHtmlUnitOptions());
- driver.getWebClient().getOptions().setCssEnabled(false);
+ WebClientOptions options = driver.getWebClient().getOptions();
+ options.setCssEnabled(false);
+
+ // HtmlUnit doesn't work very well with JS and it's recommended to use this settings.
+ // HtmlUnit validates all scripts and then fails. It turned off the validation.
+ options.setThrowExceptionOnScriptError(false);
+ options.setThrowExceptionOnFailingStatusCode(false);
+
return driver;
}
diff --git a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/ManagedWebDriver.java b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/ManagedWebDriver.java
index d48f824a4c3..82a62e6acdb 100644
--- a/test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/ManagedWebDriver.java
+++ b/test-framework/ui/src/main/java/org/keycloak/testframework/ui/webdriver/ManagedWebDriver.java
@@ -2,6 +2,8 @@ package org.keycloak.testframework.ui.webdriver;
import java.net.URL;
+import org.keycloak.testframework.injection.ManagedTestResource;
+
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
@@ -9,7 +11,7 @@ import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
-public class ManagedWebDriver {
+public class ManagedWebDriver extends ManagedTestResource {
private WebDriver driver;
@@ -18,9 +20,11 @@ public class ManagedWebDriver {
private PageUtils pageUtils = new PageUtils(this);
private NavigateUtils navigateUtils = new NavigateUtils(this);
private WaitUtils waitUtils = new WaitUtils(this);
+ private final BrowserTabUtils tabUtils;
public ManagedWebDriver(WebDriver driver) {
this.driver = driver;
+ this.tabUtils = new BrowserTabUtils(this);
}
public WebDriver driver() {
@@ -70,8 +74,16 @@ public class ManagedWebDriver {
return navigateUtils;
}
+ public BrowserTabUtils tabs() {
+ return tabUtils;
+ }
+
public WaitUtils waiting() {
return waitUtils;
}
+ @Override
+ public void runCleanup() {
+ tabUtils.closeTabs();
+ }
}
diff --git a/tests/base/src/test/java/org/keycloak/tests/common/TestRealmUserConfig.java b/tests/base/src/test/java/org/keycloak/tests/common/TestRealmUserConfig.java
new file mode 100644
index 00000000000..764a2d2fdb8
--- /dev/null
+++ b/tests/base/src/test/java/org/keycloak/tests/common/TestRealmUserConfig.java
@@ -0,0 +1,18 @@
+package org.keycloak.tests.common;
+
+import org.keycloak.testframework.realm.UserConfig;
+import org.keycloak.testframework.realm.UserConfigBuilder;
+
+/**
+ * User configuration compatible with the user test-user@localhost from testrealm.json from the old arquillian testsuite.
+ */
+public class TestRealmUserConfig implements UserConfig {
+
+ @Override
+ public UserConfigBuilder configure(UserConfigBuilder user) {
+ return user.username("test-user@localhost")
+ .password("password")
+ .email("test-user@localhost")
+ .name("Tom", "Brady");
+ }
+}
diff --git a/tests/base/src/test/java/org/keycloak/tests/model/AuthenticationSessionProviderTest.java b/tests/base/src/test/java/org/keycloak/tests/model/AuthenticationSessionProviderTest.java
index 246abe35d12..81f03913d45 100644
--- a/tests/base/src/test/java/org/keycloak/tests/model/AuthenticationSessionProviderTest.java
+++ b/tests/base/src/test/java/org/keycloak/tests/model/AuthenticationSessionProviderTest.java
@@ -40,9 +40,9 @@ import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.realm.RealmConfigBuilder;
import org.keycloak.testframework.remote.annotations.TestOnServer;
+import org.keycloak.testframework.remote.providers.timeoffset.InfinispanTimeUtil;
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
-import org.keycloak.tests.utils.infinispan.InfinispanTimeUtil;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
diff --git a/tests/base/src/test/java/org/keycloak/tests/model/UserSessionProviderTest.java b/tests/base/src/test/java/org/keycloak/tests/model/UserSessionProviderTest.java
index db0c2829edb..a1f7aeff8aa 100644
--- a/tests/base/src/test/java/org/keycloak/tests/model/UserSessionProviderTest.java
+++ b/tests/base/src/test/java/org/keycloak/tests/model/UserSessionProviderTest.java
@@ -47,9 +47,9 @@ import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.realm.RealmConfigBuilder;
import org.keycloak.testframework.remote.annotations.TestOnServer;
+import org.keycloak.testframework.remote.providers.timeoffset.InfinispanTimeUtil;
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
-import org.keycloak.tests.utils.infinispan.InfinispanTimeUtil;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenRevokeTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenRevokeTest.java
new file mode 100644
index 00000000000..b71138380d1
--- /dev/null
+++ b/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenRevokeTest.java
@@ -0,0 +1,586 @@
+package org.keycloak.tests.oauth;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.keycloak.OAuthErrorException;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.models.Constants;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserSessionRepresentation;
+import org.keycloak.testframework.annotations.InjectEvents;
+import org.keycloak.testframework.annotations.InjectRealm;
+import org.keycloak.testframework.annotations.InjectUser;
+import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
+import org.keycloak.testframework.events.EventAssertion;
+import org.keycloak.testframework.events.Events;
+import org.keycloak.testframework.oauth.OAuthClient;
+import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
+import org.keycloak.testframework.realm.ManagedRealm;
+import org.keycloak.testframework.realm.ManagedUser;
+import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
+import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
+import org.keycloak.testframework.ui.annotations.InjectWebDriver;
+import org.keycloak.testframework.ui.webdriver.BrowserTabUtils;
+import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
+import org.keycloak.tests.common.TestRealmUserConfig;
+import org.keycloak.testsuite.util.AccountHelper;
+import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
+import org.keycloak.testsuite.util.oauth.IntrospectionResponse;
+import org.keycloak.testsuite.util.oauth.UserInfoResponse;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.keycloak.tests.oauth.RefreshTokenTest.enableRefreshTokenEvents;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Test for the scenarios of refresh-token with "revokeRefreshToken" enabled on realm
+ */
+@KeycloakIntegrationTest
+public class RefreshTokenRevokeTest {
+
+ @InjectOAuthClient
+ OAuthClient oauth;
+
+ @InjectWebDriver
+ ManagedWebDriver driver;
+
+ @InjectEvents
+ Events events;
+
+ @InjectTimeOffSet
+ TimeOffSet timeOffSet;
+
+ @InjectRealm(config = RefreshTokenTest.RefreshTokenTestRealmConfig.class)
+ protected ManagedRealm realm;
+
+ @InjectUser(config = TestRealmUserConfig.class)
+ protected ManagedUser user;
+
+ @BeforeEach
+ public void before() {
+ enableRefreshTokenEvents(realm);
+ AccountHelper.logout(realm.admin(), user.getUsername());
+ }
+
+
+ @Test
+ public void refreshTokenReuseTokenWithoutRefreshTokensRevoked() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
+ RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+ assertEquals(200, response2.getStatusCode());
+
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken1.getId())
+ .type(EventType.REFRESH_TOKEN);
+
+ AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+
+ assertEquals(200, response3.getStatusCode());
+
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken1.getId())
+ .type(EventType.REFRESH_TOKEN);
+ }
+
+
+ @Test
+ public void refreshTokenReuseTokenWithRefreshTokensRevoked() {
+ realm.updateWithCleanup(r -> r.revokeRefreshToken(true));
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
+ RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+ RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
+
+ assertEquals(200, response2.getStatusCode());
+
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken1.getId())
+ .type(EventType.REFRESH_TOKEN);
+
+ AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+
+ assertEquals(400, response3.getStatusCode());
+
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken1.getId())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error("invalid_token");
+
+ // Client session invalidated hence old refresh token not valid anymore
+ AccessTokenResponse response4 = oauth.doRefreshTokenRequest(response2.getRefreshToken());
+ assertEquals(400, response4.getStatusCode());
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken2.getId())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error("invalid_token");
+ }
+
+
+ @Test
+ public void refreshTokenReuseOnDifferentTab() {
+ BrowserTabUtils browserTabs = driver.tabs();
+
+ realm.updateWithCleanup(r -> r.revokeRefreshToken(true));
+
+ //login with tab 1
+ oauth.doLogin("test-user@localhost", "password");
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
+ RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+ assertNotNull(refreshToken1.getOtherClaims().get(Constants.REUSE_ID));
+ assertNotEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshToken1.getId());
+
+ //login with tab 2
+ browserTabs.newTab(oauth.loginForm().build());
+ MatcherAssert.assertThat(browserTabs.getCountOfTabs(), Matchers.equalTo(2));
+
+ loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+ String sessionId = loginEvent.getSessionId();
+ code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse responseNew = oauth.doAccessTokenRequest(code);
+ RefreshToken refreshTokenNew = oauth.parseRefreshToken(responseNew.getRefreshToken());
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+ assertNotNull(refreshToken1.getOtherClaims().get(Constants.REUSE_ID));
+ assertNotEquals(refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew.getId());
+ assertNotEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID));
+
+ timeOffSet.set(10);
+
+ //refresh with token from tab 1
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+ assertEquals(200, response2.getStatusCode());
+ RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.REFRESH_TOKEN)
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken1.getId())
+ .details(Details.UPDATED_REFRESH_TOKEN_ID, refreshToken2.getId());
+
+ assertNotEquals(refreshToken2.getOtherClaims().get(Constants.REUSE_ID), refreshToken2.getId());
+ assertEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshToken2.getOtherClaims().get(Constants.REUSE_ID));
+
+ //refresh with token from tab 2
+ AccessTokenResponse responseNew1 = oauth.doRefreshTokenRequest(responseNew.getRefreshToken());
+ assertEquals(200, responseNew1.getStatusCode());
+ RefreshToken refreshTokenNew1 = oauth.parseRefreshToken(responseNew1.getRefreshToken());
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshTokenNew.getId())
+ .type(EventType.REFRESH_TOKEN);
+
+ assertNotEquals(refreshTokenNew1.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew1.getId());
+ assertEquals(refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew1.getOtherClaims().get(Constants.REUSE_ID));
+
+ //try refresh token reuse with token from tab 2
+ responseNew1 = oauth.doRefreshTokenRequest(responseNew.getRefreshToken());
+ assertEquals(400, responseNew1.getStatusCode());
+ }
+
+ @Test
+ public void refreshTokenReuseTokenWithRefreshTokensRevokedAfterSingleReuse() {
+ realm.updateWithCleanup(r ->
+ r.revokeRefreshToken(true)
+ .refreshTokenMaxReuse(1)
+
+ );
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse initialResponse = oauth.doAccessTokenRequest(code);
+ RefreshToken initialRefreshToken = oauth.parseRefreshToken(initialResponse.getRefreshToken());
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ // Initial refresh.
+ AccessTokenResponse responseFirstUse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
+ RefreshToken newTokenFirstUse = oauth.parseRefreshToken(responseFirstUse.getRefreshToken());
+
+ assertEquals(200, responseFirstUse.getStatusCode());
+
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, initialRefreshToken.getId())
+ .type(EventType.REFRESH_TOKEN);
+
+ // Second refresh (allowed).
+ AccessTokenResponse responseFirstReuse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
+ RefreshToken newTokenFirstReuse = oauth.parseRefreshToken(responseFirstReuse.getRefreshToken());
+ String userId = newTokenFirstReuse.getSubject();
+
+ assertEquals(200, responseFirstReuse.getStatusCode());
+
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, initialRefreshToken.getId())
+ .details(Details.REFRESH_TOKEN_SUB, userId)
+ .type(EventType.REFRESH_TOKEN);
+
+ // Token reused twice, became invalid.
+ AccessTokenResponse responseSecondReuse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
+
+ assertEquals(400, responseSecondReuse.getStatusCode());
+
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, initialRefreshToken.getId())
+ .details(Details.REFRESH_TOKEN_SUB, userId)
+ .type(EventType.REFRESH_TOKEN_ERROR).error("invalid_token");
+
+ // Refresh token from first use became invalid.
+ AccessTokenResponse responseUseOfInvalidatedRefreshToken =
+ oauth.doRefreshTokenRequest(responseFirstUse.getRefreshToken());
+
+ assertEquals(400, responseUseOfInvalidatedRefreshToken.getStatusCode());
+
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, newTokenFirstUse.getId())
+ .details(Details.REFRESH_TOKEN_SUB, userId)
+ .type(EventType.REFRESH_TOKEN_ERROR).error("invalid_token");
+
+ // Refresh token from reuse is not valid. Client session was invalidated
+ AccessTokenResponse responseUseOfValidRefreshToken =
+ oauth.doRefreshTokenRequest(responseFirstReuse.getRefreshToken());
+
+ assertEquals(400, responseUseOfValidRefreshToken.getStatusCode());
+
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, newTokenFirstReuse.getId())
+ .details(Details.REFRESH_TOKEN_SUB, userId)
+ .type(EventType.REFRESH_TOKEN_ERROR).error("invalid_token");
+ }
+
+ @Test
+ public void refreshTokenReuseOfExistingTokenAfterEnablingReuseRevokation() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse initialResponse = oauth.doAccessTokenRequest(code);
+ RefreshToken initialRefreshToken = oauth.parseRefreshToken(initialResponse.getRefreshToken());
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ // Infinite reuse allowed
+ processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
+ processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
+ processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
+
+ realm.updateWithCleanup(r ->
+ r.revokeRefreshToken(true)
+ .refreshTokenMaxReuse(1)
+
+ );
+
+ // Config changed, we start tracking reuse.
+ processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
+ processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
+
+ AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
+
+ assertEquals(400, responseReuseExceeded.getStatusCode());
+
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, initialRefreshToken.getId())
+ .details(Details.REFRESH_TOKEN_SUB, user.getId())
+ .type(EventType.REFRESH_TOKEN_ERROR).error("invalid_token");
+ }
+
+ private void processExpectedValidRefresh(String sessionId, RefreshToken requestToken, String refreshToken) {
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(refreshToken);
+ assertEquals(200, response2.getStatusCode());
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, requestToken.getId())
+ .type(EventType.REFRESH_TOKEN);
+ }
+
+ // Returns true if "test-user@localhost" has any user session with client session for "test-app"
+ private boolean hasClientSessionForTestApp() {
+ List userSessions = user.admin().getUserSessions();
+ return userSessions.stream()
+ .anyMatch(userSession -> userSession.getClients().containsValue("test-app"));
+ }
+
+
+ @Test
+ public void refreshTokenReuseOfExistingTokenAfterDisablingReuseRevokation() {
+ realm.updateWithCleanup(r ->
+ r.revokeRefreshToken(true)
+ .refreshTokenMaxReuse(1)
+
+ );
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse initialResponse = oauth.doAccessTokenRequest(code);
+ RefreshToken initialRefreshToken = oauth.parseRefreshToken(initialResponse.getRefreshToken());
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ // Single reuse authorized.
+ processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
+ processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
+
+ AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
+
+ assertEquals(400, responseReuseExceeded.getStatusCode());
+
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, initialRefreshToken.getId())
+ .type(EventType.REFRESH_TOKEN_ERROR).error("invalid_token");
+
+ RealmRepresentation realmRep = realm.admin().toRepresentation();
+ realmRep.setRevokeRefreshToken(false);
+ realm.admin().update(realmRep);
+
+ // Config changed, token cannot be used again at this point due the client session invalidated
+ AccessTokenResponse responseReuseExceeded2 = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
+ assertEquals(400, responseReuseExceeded2.getStatusCode());
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, initialRefreshToken.getId())
+ .type(EventType.REFRESH_TOKEN_ERROR).error("invalid_token");
+ }
+
+ // Doublecheck that with "revokeRefreshToken" and revoked tokens, the SSO re-authentication won't cause old tokens to be valid again
+ @Test
+ public void refreshTokenReuseTokenWithRefreshTokensRevokedAndSSOReauthentication() throws Exception {
+ // Initial login
+ realm.updateWithCleanup(r ->
+ r.revokeRefreshToken(true)
+ );
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
+ RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ // Refresh token for the first time - should pass
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+ RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
+
+ assertEquals(200, response2.getStatusCode());
+
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken1.getId())
+ .type(EventType.REFRESH_TOKEN);
+
+ // Client sessions is available now
+ assertTrue(hasClientSessionForTestApp());
+
+ // Refresh token for the second time - should fail and invalidate client session
+
+ AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+
+ assertEquals(400, response3.getStatusCode());
+
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken1.getId())
+ .type(EventType.REFRESH_TOKEN_ERROR).error("invalid_token");
+
+ // No client sessions available after revoke
+ assertFalse(hasClientSessionForTestApp());
+
+ // Introspection with the accessToken from the first authentication. This should fail
+ IntrospectionResponse introspectionResponse = oauth.doIntrospectionAccessTokenRequest(response1.getAccessToken());
+ assertFalse(introspectionResponse.asTokenMetadata().isActive());
+ events.clear();
+
+ // SSO re-authentication
+ timeOffSet.set(2);
+ oauth.openLoginForm();
+
+ loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId());
+ sessionId = loginEvent.getSessionId();
+ code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response4 = oauth.doAccessTokenRequest(code);
+ oauth.parseRefreshToken(response4.getRefreshToken());
+ events.clear();
+
+ // Client sessions should be available again now after re-authentication
+ assertTrue(hasClientSessionForTestApp());
+
+ // Introspection again with the accessToken from the very first authentication. This should fail as the access token was obtained for the old client session before SSO re-authentication
+ introspectionResponse = oauth.doIntrospectionAccessTokenRequest(response1.getAccessToken());
+ assertFalse(introspectionResponse.asTokenMetadata().isActive());
+ events.clear();
+
+ // Try userInfo with the same old access token. Should fail as well
+ UserInfoResponse userInfo = oauth.doUserInfoRequest(response1.getAccessToken());
+ assertEquals(401, userInfo.getStatusCode());
+ assertEquals(OAuthErrorException.INVALID_TOKEN, userInfo.getError());
+
+ events.clear();
+
+ // Try to refresh with one of the old refresh tokens before SSO re-authentication - should fail
+ AccessTokenResponse response5 = oauth.doRefreshTokenRequest(response2.getRefreshToken());
+ assertEquals(400, response5.getStatusCode());
+ EventAssertion.assertError(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken2.getId())
+ .type(EventType.REFRESH_TOKEN_ERROR).error("invalid_token");
+ }
+
+
+ // GH issue 45647
+ @Test
+ public void refreshTokenRevokeWithConcurrentRefreshTokenRequests() {
+ realm.updateWithCleanup(r -> r.revokeRefreshToken(true));
+ testRefreshTokenConcurrentReuse(1);
+ }
+
+ // GH issue 45647
+ @Test
+ public void refreshTokenRevokeAndReuseWithConcurrentRefreshTokenRequests() {
+ realm.updateWithCleanup(r ->
+ r.revokeRefreshToken(true)
+ .refreshTokenMaxReuse(2)
+
+ );
+ testRefreshTokenConcurrentReuse(3);
+ }
+
+ private void testRefreshTokenConcurrentReuse(int expectedSuccessfulRefreshes) {
+ int THREADS_COUNT = 5;
+
+ oauth.doLogin("test-user@localhost", "password");
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
+
+ // Try concurrent requests for refresh-token requests
+ AtomicInteger successCounts = new AtomicInteger(0);
+ AtomicInteger errorCounts = new AtomicInteger(0);
+ Runnable runnable = () -> {
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+ if (response2.getStatusCode() == 200) {
+ successCounts.incrementAndGet();
+ } else {
+ assertEquals(400, response2.getStatusCode());
+ errorCounts.incrementAndGet();
+ }
+ };
+
+ List threads = new ArrayList<>();
+ for (int i = 0 ; i < THREADS_COUNT ; i++) {
+ threads.add(new Thread(runnable));
+ }
+ for (Thread t : threads) {
+ t.start();
+ }
+ for (Thread t : threads) {
+ try {
+ t.join();
+ } catch(InterruptedException ie) {
+ fail("Interrupted exception thrown in one of the threads during token refresh");
+ }
+ }
+
+ // Check expected successful count of refreshes
+ assertEquals( expectedSuccessfulRefreshes, successCounts.get(), "Expected only " + expectedSuccessfulRefreshes + " successful refreshes, but was successfully refreshed " + successCounts.get() + " times");
+ assertEquals(THREADS_COUNT - expectedSuccessfulRefreshes, errorCounts.get());
+ }
+
+}
diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTest.java
new file mode 100755
index 00000000000..1550def7b23
--- /dev/null
+++ b/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTest.java
@@ -0,0 +1,1185 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.tests.oauth;
+
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RealmsResource;
+import org.keycloak.common.enums.SslRequired;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.cookie.CookieType;
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.jose.jws.JWSHeader;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.models.AccountRoles;
+import org.keycloak.models.Constants;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCConfigAttributes;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientScopeRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testframework.annotations.InjectAdminClient;
+import org.keycloak.testframework.annotations.InjectClient;
+import org.keycloak.testframework.annotations.InjectEvents;
+import org.keycloak.testframework.annotations.InjectRealm;
+import org.keycloak.testframework.annotations.InjectUser;
+import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
+import org.keycloak.testframework.events.EventAssertion;
+import org.keycloak.testframework.events.Events;
+import org.keycloak.testframework.oauth.OAuthClient;
+import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
+import org.keycloak.testframework.realm.ClientConfigBuilder;
+import org.keycloak.testframework.realm.ManagedClient;
+import org.keycloak.testframework.realm.ManagedRealm;
+import org.keycloak.testframework.realm.ManagedUser;
+import org.keycloak.testframework.realm.RealmConfig;
+import org.keycloak.testframework.realm.RealmConfigBuilder;
+import org.keycloak.testframework.realm.UserConfigBuilder;
+import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
+import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
+import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
+import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
+import org.keycloak.testframework.ui.annotations.InjectPage;
+import org.keycloak.testframework.ui.annotations.InjectWebDriver;
+import org.keycloak.testframework.ui.page.LoginPage;
+import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
+import org.keycloak.testframework.util.ApiUtil;
+import org.keycloak.tests.common.TestRealmUserConfig;
+import org.keycloak.tests.utils.Assert;
+import org.keycloak.testsuite.util.AccountHelper;
+import org.keycloak.testsuite.util.oauth.AbstractHttpPostRequest;
+import org.keycloak.testsuite.util.oauth.AbstractOAuthClient;
+import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.jboss.logging.Logger;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.Cookie;
+
+import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
+import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
+import static org.keycloak.events.Errors.INVALID_REQUEST;
+import static org.keycloak.tests.oauth.RefreshTokenTimeoutsTest.isPersistentSessionsFeatureEnabled;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author Stian Thorgersen
+ */
+@KeycloakIntegrationTest
+public class RefreshTokenTest {
+
+ public static final int ALLOWED_CLOCK_SKEW = 3;
+
+ private static final Logger log = Logger.getLogger(RefreshTokenTest.class);
+
+ @InjectOAuthClient
+ OAuthClient oauth;
+
+ @InjectAdminClient(mode = InjectAdminClient.Mode.BOOTSTRAP)
+ Keycloak adminClient;
+
+ @InjectRunOnServer
+ RunOnServerClient runOnServer;
+
+ @InjectWebDriver
+ ManagedWebDriver driver;
+
+ @InjectEvents
+ Events events;
+
+ @InjectTimeOffSet
+ TimeOffSet timeOffSet;
+
+ @InjectPage
+ LoginPage loginPage;
+
+ @InjectRealm(config = RefreshTokenTestRealmConfig.class)
+ protected ManagedRealm realm;
+
+ @InjectUser(config = TestRealmUserConfig.class)
+ protected ManagedUser user;
+
+ @InjectClient(attachTo = "test-app")
+ ManagedClient managedClient;
+
+ public static class RefreshTokenTestRealmConfig implements RealmConfig {
+
+ @Override
+ public RealmConfigBuilder configure(RealmConfigBuilder realm) {
+ realm.addClient("service-account-app")
+ .serviceAccountsEnabled(true)
+ .attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "true")
+ .secret("secret");
+ realm.addRole("user");
+ return realm;
+ }
+ }
+
+ @BeforeEach
+ public void before() {
+ enableRefreshTokenEvents(realm);
+ AccountHelper.logout(realm.admin(), user.getUsername());
+ }
+
+ public static void enableRefreshTokenEvents(ManagedRealm realm) {
+ RealmEventsConfigRepresentation realmEventsConfig = realm.admin().getRealmEventsConfig();
+ List enabledEventTypes = realmEventsConfig.getEnabledEventTypes();
+ if (!enabledEventTypes.contains(EventType.REFRESH_TOKEN.name())) {
+ enabledEventTypes.addAll(List.of(EventType.REFRESH_TOKEN.name(), EventType.REFRESH_TOKEN_ERROR.name()));
+ realm.admin().updateRealmEventsConfig(realmEventsConfig);
+ }
+ }
+
+ /**
+ * KEYCLOAK-547
+ *
+ */
+ @Test
+ public void nullRefreshToken() {
+ class RefreshRequestWithoutRefreshTokenParameter extends AbstractHttpPostRequest {
+
+ RefreshRequestWithoutRefreshTokenParameter(AbstractOAuthClient> client) {
+ super(client);
+ }
+
+ @Override
+ protected String getEndpoint() {
+ return client.getEndpoints().getToken();
+ }
+
+ protected void initRequest() {
+ parameter(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN);
+ scope(false);
+ }
+
+ @Override
+ protected AccessTokenResponse toResponse(CloseableHttpResponse response) throws IOException {
+ return new AccessTokenResponse(response);
+ }
+
+ }
+ AccessTokenResponse response = new RefreshRequestWithoutRefreshTokenParameter(oauth).send();
+ assertEquals(400, response.getStatusCode());
+ assertEquals("invalid_request", response.getError());
+ events.clear();
+ }
+
+ @Test
+ public void invalidRefreshToken() {
+ AccessTokenResponse response = oauth.doRefreshTokenRequest("invalid");
+ assertEquals(400, response.getStatusCode());
+ assertEquals("invalid_grant", response.getError());
+ events.clear();
+ }
+
+ @Test
+ public void refreshTokenStructure() {
+ oauth.loginForm().nonce("123456").doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+ assertNull(token.getNonce());
+
+ IDToken idToken = oauth.verifyToken(tokenResponse.getIdToken());
+ assertEquals("123456", idToken.getNonce());
+
+ String refreshTokenString = tokenResponse.getRefreshToken();
+ RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ assertNotNull(refreshTokenString);
+
+ assertNull(refreshToken.getNonce());
+ assertNull(refreshToken.getRealmAccess(), "RealmAccess should be null for RefreshTokens");
+ assertTrue(refreshToken.getResourceAccess().isEmpty(), "ResourceAccess should be null for RefreshTokens");
+ }
+
+ @Test
+ public void refreshTokenRequest() {
+ RoleRepresentation userRole = this.realm.admin().roles().get("user").toRepresentation();
+ this.user.admin().roles().realmLevel().add(List.of(userRole));
+
+ oauth.loginForm().nonce("123456").doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId())
+ .clientId("test-app")
+ .hasSessionId()
+ .type(EventType.LOGIN);
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+ assertNull(token.getNonce());
+
+ IDToken idToken = oauth.verifyToken(tokenResponse.getIdToken(), IDToken.class);
+ assertEquals("123456", idToken.getNonce());
+
+ assertNotNull(tokenResponse.getRefreshToken());
+ RefreshToken refreshToken = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
+
+ EventRepresentation tokenEvent = events.poll();
+ EventAssertion.assertSuccess(tokenEvent)
+ .userId(user.getId())
+ .sessionId(sessionId)
+ .isCodeId()
+ .clientId("test-app")
+ .type(EventType.CODE_TO_TOKEN);
+
+ assertEquals("Bearer", tokenResponse.getTokenType());
+
+ assertThat(token.getExp() - Time.currentTime(), allOf(greaterThanOrEqualTo(200L), lessThanOrEqualTo(350L)));
+ long actual = refreshToken.getExp() - Time.currentTime();
+ assertThat(actual, allOf(greaterThanOrEqualTo(1799L - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + ALLOWED_CLOCK_SKEW)));
+
+ assertEquals(sessionId, refreshToken.getSessionId());
+ assertNull(refreshToken.getNonce());
+
+ AccessTokenResponse response = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(response.getRefreshToken());
+
+ assertEquals(200, response.getStatusCode());
+
+ assertEquals(sessionId, refreshedToken.getSessionId());
+ assertEquals(sessionId, refreshedRefreshToken.getSessionId());
+
+ assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
+ assertThat(refreshedToken.getExp() - Time.currentTime(), allOf(greaterThanOrEqualTo(250L - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(300L + ALLOWED_CLOCK_SKEW)));
+
+ assertThat(refreshedToken.getExp() - token.getExp(), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(10L)));
+ assertThat(refreshedRefreshToken.getExp() - refreshToken.getExp(), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(10L)));
+
+ // "test-app" should not be an audience in the refresh token
+ assertEquals("test-app", refreshedRefreshToken.getIssuedFor());
+ assertFalse(refreshedRefreshToken.hasAudience("test-app"));
+
+ assertNotEquals(token.getId(), refreshedToken.getId());
+ assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId());
+
+ assertEquals("Bearer", response.getTokenType());
+
+ Assert.assertEquals(user.getId(), refreshedToken.getSubject());
+ assertNotEquals("test-user@localhost", refreshedToken.getSubject());
+
+ assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
+
+ assertTrue(refreshedToken.getResourceAccess(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).isUserInRole(AccountRoles.MANAGE_ACCOUNT));
+
+ EventRepresentation refreshEvent = events.poll();
+ EventAssertion.assertSuccess(refreshEvent)
+ .userId(user.getId())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .clientId("test-app")
+ .type(EventType.REFRESH_TOKEN);
+ assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
+ assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
+
+ assertNull(refreshedToken.getNonce());
+
+ idToken = oauth.verifyToken(response.getIdToken(), IDToken.class);
+ assertNull(idToken.getNonce()); // null after refresh as recommended by spec
+
+ assertNotNull(response.getRefreshToken());
+ refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
+ assertEquals(sessionId, refreshToken.getSessionId());
+ assertNull(refreshToken.getNonce());
+ }
+
+ @Test
+ public void refreshTokenWithDifferentIssuer() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.LOGIN);
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ String refreshTokenString = response.getRefreshToken();
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ String invalidIssuerRefreshToken = encodeRefreshToken(refreshTokenString);
+ response = oauth.doRefreshTokenRequest(invalidIssuerRefreshToken);
+
+ Assert.assertEquals(400, response.getStatusCode());
+ Assert.assertEquals("invalid_grant", response.getError());
+ assertThat(response.getErrorDescription(), Matchers.startsWith("Invalid token issuer."));
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error(Errors.INVALID_TOKEN);
+ }
+
+ private String encodeRefreshToken(String encodedRefreshToken) {
+ return runOnServer.fetchString(session -> {
+ try {
+ JWSInput input = new JWSInput(encodedRefreshToken);
+ RefreshToken refreshToken = input.readJsonContent(RefreshToken.class);
+
+ refreshToken.issuer("https://fake-issuer");
+ return session.tokens().encode(refreshToken);
+ } catch (JWSInputException ioe) {
+ throw new RuntimeException("Failed to encode token: " + encodedRefreshToken);
+ }
+ });
+ }
+
+
+ @Test
+ public void refreshingTokenLoadsSessionIntoCache() {
+ Assumptions.assumeTrue(isPersistentSessionsFeatureEnabled(adminClient), "Skip as persistent_user_sessions feature is disabled");
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ String refreshTokenString = response.getRefreshToken();
+
+ // Test when neither client nor user session is in the cache
+ runOnServer.run(session -> {
+ session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).clear();
+ session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).clear();
+ });
+
+ response = oauth.doRefreshTokenRequest(refreshTokenString);
+ Assert.assertEquals(200, response.getStatusCode());
+
+ runOnServer.run(session -> {
+ MatcherAssert.assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).size(),
+ greaterThan(0));
+ MatcherAssert.assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).size(),
+ greaterThan(0));
+ });
+
+ // Test is only the client session is missing
+ runOnServer.run(session -> session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).clear());
+
+ response = oauth.doRefreshTokenRequest(refreshTokenString);
+ Assert.assertEquals(200, response.getStatusCode());
+
+ runOnServer.run(session -> {
+ MatcherAssert.assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).size(),
+ greaterThan(0));
+ MatcherAssert.assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).size(),
+ greaterThan(0));
+ });
+
+ }
+
+ @Test
+ public void refreshTokenWithAccessToken() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ String accessTokenString = tokenResponse.getAccessToken();
+
+ AccessTokenResponse response = oauth.doRefreshTokenRequest(accessTokenString);
+ Assert.assertNotEquals(200, response.getStatusCode());
+ }
+
+ @Test
+ public void testDoNotResolveOfflineUserSessionIfAuthenticationSessionIsInvalidated() {
+ oauth.scope("offline_access");
+ try {
+ testDoNotResolveUserSessionIfAuthenticationSessionIsInvalidated();
+ } finally {
+ oauth.scope(null);
+ }
+ }
+
+ @Test
+ public void testDoNotResolveUserSessionIfAuthenticationSessionIsInvalidated() {
+ String realmName = KeycloakModelUtils.generateId();
+ RealmsResource realmsResource = adminClient.realms();
+ RealmRepresentation realm = new RealmRepresentation();
+ realm.setRealm(realmName);
+ realm.setEnabled(true);
+ realmsResource.create(realm);
+ RealmResource realmResource = realmsResource.realm(realmName);
+ realm = realmResource.toRepresentation();
+
+ String origRealm = oauth.getRealm();
+ String origClientId = oauth.getClientId();
+ String origClientSecret = oauth.config().getClientSecret();
+
+ try {
+ realm.setSsoSessionMaxLifespan((int) TimeUnit.MINUTES.toSeconds(2));
+ realm.setSsoSessionIdleTimeout((int) TimeUnit.MINUTES.toSeconds(2));
+ realm.setAccessTokenLifespan((int) TimeUnit.MINUTES.toSeconds(1));
+ realmResource.update(realm);
+
+ realmResource.clients().create(ClientConfigBuilder.create()
+ .clientId("public-client")
+ .redirectUris("*")
+ .publicClient(true)
+ .build()).close();
+
+ realmResource.users()
+ .create(UserConfigBuilder.create().username("alice")
+ .firstName("alice")
+ .lastName("alice")
+ .email("alice@keycloak.org")
+ .password("alice").roles("offline_access").build()).close();
+ realmResource.users()
+ .create(UserConfigBuilder.create().username("bob")
+ .firstName("bob")
+ .lastName("bob")
+ .email("bob@keycloak.org")
+ .password("bob").roles("offline_access").build()).close();
+
+ oauth.realm(realmName);
+ oauth.client("public-client");
+
+ oauth.doLogin("alice", "alice");
+ String aliceCode = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(aliceCode);
+ AccessToken aliceAt = oauth.verifyToken(tokenResponse.getAccessToken());
+
+ timeOffSet.set((int) TimeUnit.MINUTES.toSeconds(2));
+
+ oauth.doLogin("bob", "bob");
+ String bobCode = oauth.parseLoginResponse().getCode();
+
+ assertNotEquals(aliceCode, bobCode);
+
+ tokenResponse = oauth.doAccessTokenRequest(bobCode);
+ String refreshToken = tokenResponse.getRefreshToken();
+ tokenResponse = oauth.doRefreshTokenRequest(refreshToken);
+ AccessToken bobAt = oauth.verifyToken(tokenResponse.getAccessToken());
+
+ assertNotEquals(aliceAt.getSessionId(), bobAt.getSessionId());
+ assertEquals("bob", bobAt.getPreferredUsername());
+ } finally {
+ realmResource.remove();
+ oauth.realm(origRealm);
+ oauth.client(origClientId, origClientSecret);
+ }
+ }
+
+ @Test
+ public void testTimeoutWhenReUsingPreviousAuthenticationSession() {
+ String realmName = KeycloakModelUtils.generateId();
+ RealmsResource realmsResource = adminClient.realms();
+ realmsResource.create(RealmConfigBuilder.create().name(realmName).build());
+ RealmResource realmResource = realmsResource.realm(realmName);
+ RealmRepresentation realm = realmResource.toRepresentation();
+
+ String origRealm = oauth.getRealm();
+ String origClientId = oauth.getClientId();
+ String origClientSecret = oauth.config().getClientSecret();
+
+ try {
+ realm.setSsoSessionMaxLifespan((int) TimeUnit.MINUTES.toSeconds(2));
+ realm.setSsoSessionIdleTimeout((int) TimeUnit.MINUTES.toSeconds(2));
+ realm.setAccessTokenLifespan((int) TimeUnit.MINUTES.toSeconds(1));
+ realmResource.update(realm);
+
+ realmResource.clients().create(ClientConfigBuilder.create()
+ .clientId("public-client")
+ .redirectUris("*")
+ .publicClient(true)
+ .build()).close();
+
+ realmResource.users()
+ .create(UserConfigBuilder.create().username("alice")
+ .firstName("alice")
+ .lastName("alice")
+ .email("alice@keycloak.org")
+ .password("alice").roles("offline_access").build()).close();
+ realmResource.users()
+ .create(UserConfigBuilder.create().username("bob")
+ .firstName("bob")
+ .lastName("bob")
+ .email("bob@keycloak.org")
+ .password("bob").roles("offline_access").build()).close();
+
+ oauth.realm(realmName);
+ oauth.client("public-client");
+
+ oauth.openLoginForm();
+
+ Cookie authSessionCookie = driver.cookies().get(CookieType.AUTH_SESSION_ID.getName());
+
+ oauth.fillLoginForm("alice", "alice");
+
+ oauth.parseLoginResponse().getCode();
+ driver.cookies().deleteAll();
+
+ // Enforce login page to be able to delete cookies here (as appPage is on different domain)
+ oauth.loginForm().prompt(OIDCLoginProtocol.PROMPT_VALUE_LOGIN).open();
+ driver.cookies().deleteAll();
+
+ oauth.openLoginForm();
+ driver.cookies().add(authSessionCookie);
+ oauth.fillLoginForm("bob", "bob");
+ Assert.assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
+ } finally {
+ realmResource.remove();
+ oauth.realm(origRealm);
+ oauth.client(origClientId, origClientSecret);
+ }
+ }
+
+ /**
+ * KEYCLOAK-15437
+ */
+ @Test
+ public void tokenRefreshWithAccessTokenShouldReturnIdTokenWithAccessTokenHash() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ String refreshToken = tokenResponse.getRefreshToken();
+
+ AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshToken);
+ Assert.assertEquals(200, response.getStatusCode());
+ IDToken idToken = oauth.verifyToken(response.getIdToken());
+ Assert.assertNotNull(idToken.getAccessTokenHash(), "AccessTokenHash should not be null after token refresh");
+ }
+
+ public static void assertScopes(String expectedScope, String receivedScope) {
+ Collection expectedScopes = Arrays.stream(expectedScope.split(" ")).sorted().toList();
+ Collection receivedScopes = Arrays.stream(receivedScope.split(" ")).sorted().toList();
+ assertTrue(expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes),
+ "Not matched. Expected scopes: " + expectedScopes + ", Received scopes: " + receivedScopes);
+ }
+
+ @Test
+ public void refreshTokenReuseTokenWithoutRefreshTokensRevokedWithLessScopes() {
+ //add phone,address as optional scope and request them
+ ClientScopeRepresentation phoneScope = findClientScopeByName("phone");
+ ClientScopeRepresentation addressScope = findClientScopeByName("address");
+
+ managedClient.admin().addOptionalClientScope(phoneScope.getId());
+ managedClient.admin().addOptionalClientScope(addressScope.getId());
+
+ try {
+ oauth.doLogin("test-user@localhost", "password");
+
+ oauth.parseLoginResponse().getCode();
+
+ String optionalScope = "phone address";
+ oauth.scope(optionalScope);
+ AccessTokenResponse response1 = oauth.doPasswordGrantRequest("test-user@localhost", "password");
+ RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
+ assertScopes("openid basic email roles service_account web-origins acr profile address phone", refreshToken1.getScope());
+
+ timeOffSet.set(2);
+
+ String scope = "email phone";
+ oauth.scope(scope);
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+ assertEquals(200, response2.getStatusCode());
+ assertScopes("openid email phone profile", response2.getScope());
+ RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
+ assertNotNull(refreshToken2);
+ assertScopes("openid acr roles phone address email profile basic service_account web-origins", refreshToken2.getScope());
+
+ } finally {
+ oauth.scope(null);
+ managedClient.admin().removeOptionalClientScope(phoneScope.getId());
+ managedClient.admin().removeOptionalClientScope(addressScope.getId());
+ }
+ }
+
+ @Test
+ public void refreshTokenReuseTokenScopeParameterNotInRefreshToken() {
+ try {
+ //scope parameter consists scope that is not part of scope refresh token => error thrown
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
+ RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
+ assertScopes("openid basic email roles service_account web-origins acr profile", refreshToken1.getScope());
+
+ timeOffSet.set(2);
+
+ String scope = "openid email ssh_public_key";
+ oauth.scope(scope);
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
+ assertEquals(400, response2.getStatusCode());
+ assertEquals(OAuthErrorException.INVALID_SCOPE, response2.getError());
+
+ } finally {
+ oauth.scope(null);
+ }
+ }
+
+ private ClientScopeRepresentation findClientScopeByName(String name) {
+ return realm.admin().clientScopes().findAll().stream()
+ .filter((ClientScopeRepresentation clientScope) -> name.equals(clientScope.getName()))
+ .findFirst().get();
+ }
+
+ @Test
+ public void refreshWithOptionalClientScopeWithIncludeInTokenScopeDisabled() {
+ //set roles client scope as optional
+ ClientScopeRepresentation rolesScope = findClientScopeByName(OIDCLoginProtocolFactory.ROLES_SCOPE);
+ managedClient.admin().removeDefaultClientScope(rolesScope.getId());
+ managedClient.admin().addOptionalClientScope(rolesScope.getId());
+
+ try {
+ oauth.scope("roles");
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
+
+ assertScopes("openid email profile", accessToken.getScope());
+ assertScopes("openid basic email roles service_account web-origins acr profile", refreshToken.getScope());
+
+ Assert.assertNotNull(accessToken.getRealmAccess());
+ Assert.assertNotNull(accessToken.getResourceAccess());
+
+ oauth.scope(null);
+
+ response = oauth.doRefreshTokenRequest(response.getRefreshToken());
+
+ accessToken = oauth.verifyToken(response.getAccessToken());
+ refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
+
+ assertScopes("openid email profile", accessToken.getScope());
+ assertScopes("openid basic email roles service_account web-origins acr profile", refreshToken.getScope());
+
+ Assert.assertNotNull(accessToken.getRealmAccess());
+ Assert.assertNotNull(accessToken.getResourceAccess());
+
+ } finally {
+ managedClient.admin().removeOptionalClientScope(rolesScope.getId());
+ managedClient.admin().addDefaultClientScope(rolesScope.getId());
+ }
+ }
+
+ private void processExpectedValidRefresh(String sessionId, RefreshToken requestToken, String refreshToken) {
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(refreshToken);
+ assertEquals(200, response2.getStatusCode());
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, requestToken.getId())
+ .type(EventType.REFRESH_TOKEN);
+ }
+
+
+ @Test
+ public void refreshTokenClientDisabled() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ String refreshTokenString = response.getRefreshToken();
+ events.clear();
+
+ managedClient.updateWithCleanup(c -> c.enabled(false));
+
+ response = oauth.doRefreshTokenRequest(refreshTokenString);
+
+ assertEquals(401, response.getStatusCode());
+ assertEquals("invalid_client", response.getError());
+
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error(Errors.CLIENT_DISABLED);
+ managedClient.updateWithCleanup(c -> c.enabled(true));
+ }
+
+ @Test
+ public void refreshTokenUserSessionRemoved() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ String sessionId = tokenResponse.getSessionState();
+
+ events.clear();
+
+ realm.admin().deleteSession(sessionId, false);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error(Errors.INVALID_TOKEN);
+
+ events.clear();
+ }
+
+ @Test
+ public void refreshTokenAfterUserLogoutAndLoginAgain() {
+ String refreshToken1 = loginAndForceNewLoginPage();
+
+ oauth.doLogout(refreshToken1);
+ events.clear();
+
+
+ // Continue with login
+ timeOffSet.set(2);
+ driver.navigate().refresh();
+ oauth.fillLoginForm("test-user@localhost", "password");
+
+ AccessTokenResponse tokenResponse2;
+ String code = oauth.parseLoginResponse().getCode();
+ tokenResponse2 = oauth.doAccessTokenRequest(code);
+
+ // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
+ AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1);
+ assertEquals(400, responseReuseExceeded.getStatusCode());
+
+ // Finally try with valid refresh token
+ responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken());
+ assertEquals(200, responseReuseExceeded.getStatusCode());
+ }
+
+ @Test
+ public void refreshTokenAfterAdminLogoutAllAndLoginAgain() {
+ String refreshToken1 = loginAndForceNewLoginPage();
+
+ realm.admin().logoutAll();
+ events.clear();
+
+ // Continue with login
+ timeOffSet.set(2);
+ driver.navigate().refresh();
+ oauth.fillLoginForm("test-user@localhost", "password");
+
+ AccessTokenResponse tokenResponse2;
+ String code = oauth.parseLoginResponse().getCode();
+ tokenResponse2 = oauth.doAccessTokenRequest(code);
+
+ // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
+ AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1);
+ assertEquals(400, responseReuseExceeded.getStatusCode());
+
+ // Finally try with valid refresh token
+ responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken());
+ assertEquals(200, responseReuseExceeded.getStatusCode());
+ }
+
+ @Test
+ public void refreshTokenAfterUserAdminLogoutEndpointAndLoginAgain() {
+ String refreshToken1 = loginAndForceNewLoginPage();
+ user.admin().logout();
+
+ try {
+ // Continue with login
+ timeOffSet.set(2);
+ driver.navigate().refresh();
+ oauth.fillLoginForm("test-user@localhost", "password");
+
+ AccessTokenResponse tokenResponse2;
+ String code = oauth.parseLoginResponse().getCode();
+ tokenResponse2 = oauth.doAccessTokenRequest(code);
+
+ // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
+ AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1);
+ assertEquals(400, responseReuseExceeded.getStatusCode());
+
+ // Finally try with valid refresh token
+ responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken());
+ assertEquals(200, responseReuseExceeded.getStatusCode());
+ } finally {
+ // Need to reset not-before of user, which was updated during user.logout()
+ UserRepresentation userRep = user.admin().toRepresentation();
+ userRep.setNotBefore(0);
+ user.admin().update(userRep);
+ }
+ }
+
+
+ @Test
+ public void testCheckSsl() {
+ try {
+ AccessTokenResponse tokenResponse = oauth.doPasswordGrantRequest("test-user@localhost", "password");
+ String refreshToken = tokenResponse.getRefreshToken();
+ assertNotNull(refreshToken);
+
+ if (!oauth.getEndpoints().getIssuer().startsWith("https://")) { // test checkSsl
+ RealmRepresentation realmRep = realm.admin().toRepresentation();
+ String origSslRequired = realmRep.getSslRequired();
+ realmRep.setSslRequired(SslRequired.ALL.toString());
+ realm.admin().update(realmRep);
+
+ try {
+ AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshToken);
+ assertEquals(403, response.getStatusCode());
+ assertEquals(INVALID_REQUEST, response.getError());
+ assertEquals("HTTPS required", response.getErrorDescription());
+ } finally {
+ realmRep.setSslRequired(origSslRequired);
+ realm.admin().update(realmRep);
+ }
+ }
+
+ AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshToken);
+ assertEquals(200, response.getStatusCode());
+ assertNotNull(response.getRefreshToken());
+ } finally {
+ events.clear();
+ }
+
+ }
+
+ @Test
+ public void refreshTokenUserDisabled() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ String sessionId = response.getSessionState();
+
+ String refreshTokenString = response.getRefreshToken();
+ assertNotNull(refreshTokenString);
+ events.clear();
+
+ UserRepresentation userRep = user.admin().toRepresentation();
+
+ try {
+ userRep.setEnabled(false);
+ user.admin().update(userRep);
+
+ response = oauth.doRefreshTokenRequest(refreshTokenString);
+ assertEquals(400, response.getStatusCode());
+ assertEquals("invalid_grant", response.getError());
+
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .sessionId(sessionId)
+ .error(Errors.INVALID_TOKEN);
+ } finally {
+ userRep.setEnabled(true);
+ user.admin().update(userRep);
+ }
+ }
+
+ @Test
+ public void refreshTokenUserDeleted() {
+ UserConfigBuilder newUser = UserConfigBuilder.create()
+ .username("temp-user@localhost")
+ .password("password")
+ .name("First", "Last")
+ .email("temp-user@localhost")
+ .enabled(true);
+ UserRepresentation rep = newUser.build();
+ String userId = ApiUtil.getCreatedId(realm.admin().users().create(rep));
+
+ oauth.doLogin("temp-user@localhost", "password");
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ String sessionId = response.getSessionState();
+
+ String refreshTokenString = response.getRefreshToken();
+ assertNotNull(refreshTokenString);
+ events.clear();
+
+ realm.admin().users().delete(userId).close();
+
+ response = oauth.doRefreshTokenRequest(refreshTokenString);
+ assertEquals(400, response.getStatusCode());
+ assertEquals("invalid_grant", response.getError());
+
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .userId(null)
+ .sessionId(sessionId)
+ .error(Errors.INVALID_TOKEN);
+ }
+
+ @Test
+ public void refreshTokenServiceAccount() {
+ String origClientId = oauth.config().getClientId();
+ String origClientSecret = oauth.config().getClientSecret();
+ try {
+ AccessTokenResponse response = oauth.client("service-account-app", "secret").doClientCredentialsGrantAccessTokenRequest();
+ assertNotNull(response.getRefreshToken());
+ response = oauth.doRefreshTokenRequest(response.getRefreshToken());
+ assertNotNull(response.getRefreshToken());
+ } finally {
+ oauth.client(origClientId, origClientSecret);
+ }
+ }
+
+ @Test
+ public void refreshTokenRequestNoRefreshToken() {
+ ClientRepresentation client = managedClient.admin().toRepresentation();
+ oauth.doLogin("test-user@localhost", "password");
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+
+ String refreshTokenString = tokenResponse.getRefreshToken();
+
+ client.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false");
+ managedClient.admin().update(client);
+
+ try {
+
+ AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString);
+
+ assertNotNull(response.getAccessToken());
+ assertNull(response.getRefreshToken());
+ } finally {
+ client.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "true");
+ managedClient.admin().update(client);
+ }
+ }
+
+ @Test
+ public void tokenRefreshRequest_ClientRS384_RealmRS384() throws Exception {
+ conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.RS384, Algorithm.RS384);
+ }
+
+ @Test
+ public void tokenRefreshRequest_ClientRS512_RealmRS256() throws Exception {
+ conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.RS512, Algorithm.RS256);
+ }
+
+ @Test
+ public void tokenRefreshRequest_ClientES256_RealmRS256() throws Exception {
+ conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.ES256, Algorithm.RS256);
+ }
+
+ @Test
+ public void tokenRefreshRequest_ClientES384_RealmES384() throws Exception {
+ conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.ES384, Algorithm.ES384);
+ }
+
+ @Test
+ public void tokenRefreshRequest_ClientES512_RealmRS256() throws Exception {
+ conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.ES512, Algorithm.RS256);
+ }
+
+ @Test
+ public void tokenRefreshRequest_ClientPS256_RealmRS256() throws Exception {
+ conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.PS256, Algorithm.RS256);
+ }
+
+ @Test
+ public void tokenRefreshRequest_ClientPS384_RealmES384() throws Exception {
+ conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.PS384, Algorithm.ES384);
+ }
+
+ @Test
+ public void tokenRefreshRequest_ClientPS512_RealmPS256() throws Exception {
+ conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.PS512, Algorithm.PS256);
+ }
+
+ private void conductTokenRefreshRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
+ try {
+ // Realm setting is used for ID Token signature algorithm
+ changeRealmTokenSignatureProvider(expectedIdTokenAlg);
+ changeClientAccessTokenSignatureProvider(expectedAccessAlg);
+ refreshToken(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg);
+ } finally {
+ changeRealmTokenSignatureProvider(Constants.DEFAULT_SIGNATURE_ALGORITHM);
+ changeClientAccessTokenSignatureProvider(Constants.DEFAULT_SIGNATURE_ALGORITHM);
+ }
+ }
+
+ private void changeRealmTokenSignatureProvider(String toSigAlgName) {
+ RealmRepresentation rep = realm.admin().toRepresentation();
+ log.tracef("change realm test signature algorithm from %s to %s", rep.getDefaultSignatureAlgorithm(), toSigAlgName);
+ rep.setDefaultSignatureAlgorithm(toSigAlgName);
+ realm.admin().update(rep);
+ }
+
+ private void changeClientAccessTokenSignatureProvider(String toSigAlgName) {
+ ClientRepresentation clientRep = managedClient.admin().toRepresentation();
+ log.tracef("change client %s access token signature algorithm from %s to %s", clientRep.getClientId(), clientRep.getAttributes().get(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG), toSigAlgName);
+ clientRep.getAttributes().put(OIDCConfigAttributes.ACCESS_TOKEN_SIGNED_RESPONSE_ALG, toSigAlgName);
+ managedClient.admin().update(clientRep);
+ }
+
+ private void refreshToken(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId())
+ .clientId("test-app")
+ .hasSessionId()
+ .type(EventType.LOGIN);
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+
+ JWSHeader header = new JWSInput(tokenResponse.getAccessToken()).getHeader();
+ assertEquals(expectedAccessAlg, header.getAlgorithm().name());
+ assertEquals("JWT", header.getType());
+ assertNull(header.getContentType());
+
+ header = new JWSInput(tokenResponse.getIdToken()).getHeader();
+ assertEquals(expectedIdTokenAlg, header.getAlgorithm().name());
+ assertEquals("JWT", header.getType());
+ assertNull(header.getContentType());
+
+ header = new JWSInput(tokenResponse.getRefreshToken()).getHeader();
+ assertEquals(expectedRefreshAlg, header.getAlgorithm().name());
+ assertEquals("JWT", header.getType());
+ assertNull(header.getContentType());
+
+ AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
+ String refreshTokenString = tokenResponse.getRefreshToken();
+ RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
+
+ EventRepresentation tokenEvent = events.poll();
+ EventAssertion.assertSuccess(tokenEvent)
+ .userId(user.getId())
+ .sessionId(sessionId)
+ .isCodeId()
+ .clientId("test-app")
+ .type(EventType.CODE_TO_TOKEN);
+
+ assertNotNull(refreshTokenString);
+
+ assertEquals("Bearer", tokenResponse.getTokenType());
+
+ assertEquals(sessionId, refreshToken.getSessionId());
+
+ AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString);
+ if (response.getError() != null || response.getErrorDescription() != null) {
+ log.debugf("Refresh token error: %s, error description: %s", response.getError(), response.getErrorDescription());
+ }
+
+ AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
+ RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(response.getRefreshToken());
+
+ assertEquals(200, response.getStatusCode());
+
+ assertEquals(sessionId, refreshedToken.getSessionId());
+ assertEquals(sessionId, refreshedRefreshToken.getSessionId());
+
+ Assert.assertNotEquals(token.getId(), refreshedToken.getId());
+ Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId());
+
+ assertEquals("Bearer", response.getTokenType());
+
+ Assert.assertEquals(user.getId(), refreshedToken.getSubject());
+
+ EventRepresentation refreshEvent = events.poll();
+ EventAssertion.assertSuccess(refreshEvent)
+ .userId(user.getId())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .details(Details.UPDATED_REFRESH_TOKEN_ID, refreshedRefreshToken.getId())
+ .clientId("test-app")
+ .type(EventType.REFRESH_TOKEN);
+ }
+
+ private String loginAndForceNewLoginPage() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId())
+ .clientId("test-app")
+ .hasSessionId()
+ .type(EventType.LOGIN);
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ // Assert refresh successful
+ String refreshToken = tokenResponse.getRefreshToken();
+ RefreshToken refreshTokenParsed1 = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
+ processExpectedValidRefresh(sessionId, refreshTokenParsed1, refreshToken);
+
+ // Open the tab with prompt=login. AuthenticationSession will be created with same ID like userSession
+ oauth.loginForm()
+ .prompt(OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
+ .open();
+
+ loginPage.assertCurrent();
+ Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername());
+
+ return refreshToken;
+ }
+}
diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTimeoutsTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTimeoutsTest.java
new file mode 100644
index 00000000000..f9600ebbb98
--- /dev/null
+++ b/tests/base/src/test/java/org/keycloak/tests/oauth/RefreshTokenTimeoutsTest.java
@@ -0,0 +1,881 @@
+package org.keycloak.tests.oauth;
+
+import java.util.UUID;
+
+import jakarta.ws.rs.core.Response;
+
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.common.Profile;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.models.AuthenticatedClientSessionModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
+import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.utils.SessionTimeoutHelper;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserSessionRepresentation;
+import org.keycloak.representations.info.FeatureRepresentation;
+import org.keycloak.representations.info.ServerInfoRepresentation;
+import org.keycloak.testframework.annotations.InjectAdminClient;
+import org.keycloak.testframework.annotations.InjectClient;
+import org.keycloak.testframework.annotations.InjectEvents;
+import org.keycloak.testframework.annotations.InjectRealm;
+import org.keycloak.testframework.annotations.InjectUser;
+import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
+import org.keycloak.testframework.events.EventAssertion;
+import org.keycloak.testframework.events.Events;
+import org.keycloak.testframework.oauth.OAuthClient;
+import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
+import org.keycloak.testframework.realm.ManagedClient;
+import org.keycloak.testframework.realm.ManagedRealm;
+import org.keycloak.testframework.realm.ManagedUser;
+import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
+import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
+import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
+import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
+import org.keycloak.testframework.ui.annotations.InjectPage;
+import org.keycloak.testframework.ui.page.LoginPage;
+import org.keycloak.testframework.util.ApiUtil;
+import org.keycloak.tests.common.TestRealmUserConfig;
+import org.keycloak.tests.utils.Assert;
+import org.keycloak.testsuite.util.AccountHelper;
+import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
+
+import org.hamcrest.Matchers;
+import org.infinispan.Cache;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
+import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
+import static org.keycloak.protocol.oidc.OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT;
+import static org.keycloak.protocol.oidc.OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN;
+import static org.keycloak.tests.oauth.RefreshTokenTest.enableRefreshTokenEvents;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test for the scenarios related to refresh-token and involving userSession and clientSession timeouts (idle-timeout, max session timeout etc).
+ */
+@KeycloakIntegrationTest
+public class RefreshTokenTimeoutsTest {
+
+ @InjectOAuthClient
+ OAuthClient oauth;
+
+ @InjectAdminClient(mode = InjectAdminClient.Mode.BOOTSTRAP)
+ Keycloak adminClient;
+
+ @InjectRunOnServer
+ RunOnServerClient runOnServer;
+
+ @InjectEvents
+ Events events;
+
+ @InjectTimeOffSet(enableForCaches = true)
+ TimeOffSet timeOffSet;
+
+ @InjectPage
+ LoginPage loginPage;
+
+ @InjectRealm(config = RefreshTokenTest.RefreshTokenTestRealmConfig.class)
+ protected ManagedRealm realm;
+
+ @InjectUser(config = TestRealmUserConfig.class)
+ protected ManagedUser user;
+
+ @InjectClient(attachTo = "test-app")
+ ManagedClient managedClient;
+
+ @BeforeEach
+ public void before() {
+ enableRefreshTokenEvents(realm);
+ AccountHelper.logout(realm.admin(), user.getUsername());
+ }
+
+ @Test
+ public void testUserSessionRefreshAndIdle() {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId())
+ .clientId("test-app")
+ .type(EventType.LOGIN);
+
+ String sessionId = loginEvent.getSessionId();
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+
+ EventRepresentation tokenEvent = events.poll();
+ EventAssertion.assertSuccess(tokenEvent)
+ .userId(user.getId())
+ .sessionId(sessionId)
+ .clientId("test-app")
+ .type(EventType.CODE_TO_TOKEN);
+
+ long last = getLastSessionRefresh(sessionId);
+
+ timeOffSet.set(2);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+
+ oauth.verifyToken(tokenResponse.getAccessToken());
+ oauth.parseRefreshToken(tokenResponse.getRefreshToken());
+
+ assertEquals(200, tokenResponse.getStatusCode());
+
+ long next = getLastSessionRefresh(sessionId);
+
+ assertNotEquals(last, next);
+
+ RealmRepresentation realmRep = realm.admin().toRepresentation();
+ int lastAccessTokenLifespan = realmRep.getAccessTokenLifespan();
+ int originalIdle = realmRep.getSsoSessionIdleTimeout();
+
+ try {
+ realmRep.setAccessTokenLifespan(100000);
+ realm.admin().update(realmRep);
+
+ timeOffSet.set(4);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+
+ next = getLastSessionRefresh(sessionId);
+
+ // lastSEssionRefresh should be updated because access code lifespan is higher than sso idle timeout
+ assertThat(next, allOf(greaterThan(last), lessThan(last + 50)));
+
+ realmRep.setSsoSessionIdleTimeout(1);
+ realm.admin().update(realmRep);
+
+ events.clear();
+ // Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
+ timeOffSet.set(6 + (isPersistentSessionsFeatureEnabled() ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+
+ // test idle timeout
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error(Errors.INVALID_TOKEN);
+
+ } finally {
+ realmRep.setSsoSessionIdleTimeout(originalIdle);
+ realmRep.setAccessTokenLifespan(lastAccessTokenLifespan);
+ realm.admin().update(realmRep);
+ events.clear();
+ }
+
+ }
+
+ private long getLastSessionRefresh(String sessionId) {
+ UserSessionRepresentation userSession = user.admin().getUserSessions().stream()
+ .filter(session -> sessionId.equals(session.getId()))
+ .findAny().orElseThrow();
+ return userSession.getLastAccess() / 1000;
+ }
+
+ @Test
+ public void testUserSessionRefreshAndIdleRememberMe() {
+ realm.updateWithCleanup(r -> r
+ .setRememberMe(true)
+ .ssoSessionIdleTimeoutRememberMe(500)
+ .ssoSessionIdleTimeout(100));
+
+ oauth.openLoginForm();
+ loginPage.rememberMe(true);
+ loginPage.fillLogin("test-user@localhost", "password");
+ loginPage.submit();
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ String sessionId = tokenResponse.getSessionState();
+
+ long last = getLastSessionRefresh(sessionId);
+
+ timeOffSet.set(110 + (isPersistentSessionsFeatureEnabled() ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ oauth.verifyToken(tokenResponse.getAccessToken());
+ oauth.parseRefreshToken(tokenResponse.getRefreshToken());
+ assertEquals(200, tokenResponse.getStatusCode());
+
+ long next = getLastSessionRefresh(sessionId);
+ assertNotEquals(last, next);
+
+ events.clear();
+ // Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
+ timeOffSet.set(620 + 2 * (isPersistentSessionsFeatureEnabled() ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+
+ // test idle remember me timeout
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error(Errors.INVALID_TOKEN);
+ }
+
+
+ private String getClientSessionUuid(final String userSessionId, String clientId) {
+ String realmName = realm.getName();
+ return runOnServer.fetchString(session -> {
+ RealmModel realmModel = session.realms().getRealmByName(realmName);
+ ClientModel clientModel = realmModel.getClientByClientId(clientId);
+ UserSessionModel userSession = session.sessions().getUserSession(realmModel, userSessionId);
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
+ return clientSession.getId();
+ });
+ }
+
+ private int checkIfUserAndClientSessionExist(final String userSessionId, final String clientId, final String clientSessionId) {
+ String realmName = realm.getName();
+ return runOnServer.fetch(session -> {
+ RealmModel realmModel = session.realms().getRealmByName(realmName);
+ ClientModel clientModel = realmModel.getClientByClientId(clientId);
+ UserSessionModel userSession = session.sessions().getUserSession(realmModel, userSessionId);
+ if (userSession != null) {
+ AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
+ return clientSession != null && clientSessionId.equals(clientSession.getId())? 2 : 1;
+ }
+ return 0;
+ }, Integer.class);
+ }
+
+
+ @Test
+ public void refreshTokenUserSessionMaxLifespan() {
+ realm.updateWithCleanup(r -> r
+ .ssoSessionMaxLifespan(3600)
+ .ssoSessionIdleTimeout(7200));
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId())
+ .clientId("test-app")
+ .type(EventType.LOGIN);
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ String sessionId = tokenResponse.getSessionState();
+
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3600, "Invalid ExpiresIn");
+ final String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ events.poll();
+
+ timeOffSet.set(1800);
+
+ String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1800, "Invalid ExpiresIn");
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ EventRepresentation refreshEvent = events.poll();
+ EventAssertion.assertSuccess(refreshEvent)
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshId)
+ .type(EventType.REFRESH_TOKEN);
+
+ timeOffSet.set(3700);
+ oauth.parseRefreshToken(tokenResponse.getRefreshToken());
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error(Errors.INVALID_TOKEN);
+ assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+ }
+
+ @Test
+ public void refreshTokenUserClientMaxLifespanSmallerThanSession() {
+ realm.updateWithCleanup(r ->
+ r.ssoSessionMaxLifespan(3600)
+ .ssoSessionIdleTimeout(7200)
+ .clientSessionMaxLifespan(1000)
+ .clientSessionIdleTimeout(7200)
+ );
+
+ oauth.doLogin("test-user@localhost", "password");
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId())
+ .clientId("test-app")
+ .type(EventType.LOGIN);
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1000, "Invalid ExpiresIn");
+ String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ events.poll();
+
+ timeOffSet.set(600);
+ String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400, "Invalid ExpiresIn");
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+ expectRefreshEventSuccess(refreshId, sessionId);
+
+ timeOffSet.set(1100);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+ expectRefreshEventError();
+ assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ timeOffSet.set(1600);
+ oauth.openLoginForm();
+ loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .userId(user.getId())
+ .clientId("test-app")
+ .type(EventType.LOGIN);
+ sessionId = loginEvent.getSessionId();
+ code = oauth.parseLoginResponse().getCode();
+ tokenResponse = oauth.doAccessTokenRequest(code);
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1000, "Invalid ExpiresIn");
+ events.poll();
+
+ clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ timeOffSet.set(3700);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+ expectRefreshEventError();
+ assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+ }
+
+ private void expectRefreshEventSuccess(String refreshId, String sessionId) {
+ EventAssertion.assertSuccess(events.poll())
+ .sessionId(sessionId)
+ .details(Details.REFRESH_TOKEN_ID, refreshId)
+ .type(EventType.REFRESH_TOKEN);
+ }
+
+ private void expectRefreshEventError() {
+ EventAssertion.assertError(events.poll())
+ .type(EventType.REFRESH_TOKEN_ERROR)
+ .error(Errors.INVALID_TOKEN);
+ }
+
+ private boolean isPersistentSessionsFeatureEnabled() {
+ return isPersistentSessionsFeatureEnabled(adminClient);
+ }
+
+ static boolean isPersistentSessionsFeatureEnabled(Keycloak adminClient) {
+ ServerInfoRepresentation serverInfo = adminClient.serverInfo().getInfo();
+ FeatureRepresentation feature = serverInfo.getFeatures().stream()
+ .filter(feat -> Profile.Feature.PERSISTENT_USER_SESSIONS.name().equals(feat.getName()))
+ .findFirst().orElseThrow(() -> new RuntimeException("Persistent user sessions feature not found"));
+ return feature.isEnabled();
+ }
+
+ @Test
+ public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() {
+ isPersistentSessionsFeatureEnabled();
+ realm.updateWithCleanup(r ->
+ r.ssoSessionMaxLifespan(7200)
+ .ssoSessionIdleTimeout(7200)
+ .clientSessionMaxLifespan(7200)
+ .clientSessionIdleTimeout(7200)
+ );
+
+ oauth.doLogin("test-user@localhost", "password");
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200, "Invalid ExpiresIn");
+ final String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ events.poll();
+
+ RealmRepresentation rep = realm.admin().toRepresentation();
+ rep.setSsoSessionMaxLifespan(3600);
+ rep.setClientSessionMaxLifespan(3600);
+ realm.admin().update(rep);
+
+ timeOffSet.set(3700);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+ expectRefreshEventError();
+ //events.assertRefreshTokenErrorAndMaybeSessionExpired(sessionId, loginEvent.getUserId(), loginEvent.getClientId());
+ assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+ }
+
+ @Test
+ public void refreshTokenClientSessionMaxLifespanModifiedAfterTokenRefresh() {
+ realm.updateWithCleanup(r ->
+ r.ssoSessionMaxLifespan(7200)
+ .ssoSessionIdleTimeout(7200)
+ .clientSessionMaxLifespan(7200)
+ .clientSessionIdleTimeout(7200)
+ );
+
+ oauth.doLogin("test-user@localhost", "password");
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ assertEquals(200, tokenResponse.getStatusCode());
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200, "Invalid ExpiresIn: " + tokenResponse.getRefreshExpiresIn());
+ String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ events.poll();
+
+ RealmRepresentation rep = realm.admin().toRepresentation();
+ rep.setClientSessionMaxLifespan(3600);
+ realm.admin().update(rep);
+
+ timeOffSet.set(3700);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+ expectRefreshEventError();
+ assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ timeOffSet.set(4200);
+ oauth.openLoginForm();
+ loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+ sessionId = loginEvent.getSessionId();
+ code = oauth.parseLoginResponse().getCode();
+ tokenResponse = oauth.doAccessTokenRequest(code);
+ assertEquals(200, tokenResponse.getStatusCode());
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3000, "Invalid ExpiresIn: " + tokenResponse.getRefreshExpiresIn());
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ timeOffSet.set(7300);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+ expectRefreshEventError();
+ assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+ }
+
+ @Test
+ public void silentLoginClientSessionMaxLifespanModifiedAfterTokenRefresh() {
+ realm.updateWithCleanup(r ->
+ r.ssoSessionMaxLifespan(7200)
+ .ssoSessionIdleTimeout(7200)
+ .clientSessionMaxLifespan(7200)
+ .clientSessionIdleTimeout(7200)
+ );
+
+ oauth.doLogin("test-user@localhost", "password");
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200, "Invalid ExpiresIn");
+ String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ events.poll();
+
+ RealmRepresentation rep = realm.admin().toRepresentation();
+ rep.setClientSessionMaxLifespan(3600);
+ realm.admin().update(rep);
+
+ timeOffSet.set(4200);
+ oauth.openLoginForm();
+ loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+ sessionId = loginEvent.getSessionId();
+ code = oauth.parseLoginResponse().getCode();
+ tokenResponse = oauth.doAccessTokenRequest(code);
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3000, "Invalid ExpiresIn");
+ EventAssertion.assertSuccess(events.poll())
+ .type(EventType.CODE_TO_TOKEN);
+
+ clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
+ assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+
+ timeOffSet.set(7300);
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+ expectRefreshEventError();
+ assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
+ }
+
+ /**
+ * KEYCLOAK-1267
+ */
+ @Test
+ public void refreshTokenUserSessionMaxLifespanWithRememberMe() {
+ realm.updateWithCleanup(r -> r
+ .setRememberMe(true)
+ .ssoSessionMaxLifespanRememberMe(100)
+ .ssoSessionMaxLifespan(50));
+
+ oauth.openLoginForm();
+ loginPage.rememberMe(true);
+ loginPage.fillLogin("test-user@localhost", "password");
+ loginPage.submit();
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+
+ events.poll();
+
+ timeOffSet.set(110);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ expectRefreshEventError();
+ events.clear();
+ }
+
+
+ @Test
+ public void refreshTokenClientSessionMaxLifespan() {
+ RealmResource realm = this.realm.admin();
+ RealmRepresentation rep = realm.toRepresentation();
+ Integer originalSsoSessionMaxLifespan = rep.getSsoSessionMaxLifespan();
+
+ ClientResource client = managedClient.admin();
+ ClientRepresentation clientRepresentation = client.toRepresentation();
+
+ try {
+ rep.setSsoSessionMaxLifespan(1000);
+ realm.update(rep);
+
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_MAX_LIFESPAN, "500");
+ client.update(clientRepresentation);
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+
+ events.poll();
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 500, "Invalid RefreshExpiresIn" + tokenResponse.getRefreshExpiresIn());
+
+ timeOffSet.set(100);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400, "Invalid RefreshExpiresIn");
+
+ timeOffSet.set(600);
+
+ oauth.openLoginForm();
+ code = oauth.parseLoginResponse().getCode();
+
+ tokenResponse = oauth.doAccessTokenRequest(code);
+ assertEquals(200, tokenResponse.getStatusCode());
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400, "Invalid RefreshExpiresIn" + tokenResponse.getRefreshExpiresIn());
+
+ timeOffSet.set(700);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertEquals(200, tokenResponse.getStatusCode());
+ assertTrue(0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 300, "Invalid RefreshExpiresIn" + tokenResponse.getRefreshExpiresIn());
+
+ timeOffSet.set(1100);
+
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+
+ expectRefreshEventError();
+ } finally {
+ rep.setSsoSessionMaxLifespan(originalSsoSessionMaxLifespan);
+ realm.update(rep);
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_MAX_LIFESPAN, null);
+ client.update(clientRepresentation);
+ }
+ }
+
+ /**
+ * This is a very esoteric test specific to bug #38591.
+ * Consider removing or rewriting the test if the loading of sessions from the database has changed and no longer
+ * updates the client session timestamp. It is also specific to the case when the idle timeout of a client is reduced
+ * while some client sessions already exist.
+ */
+ @Test
+ public void refreshTokenClientSessionIdleTimeoutTwoClientsWithReloadingFromDatabase() {
+ Assumptions.assumeTrue(isPersistentSessionsFeatureEnabled(), "Skip as persistent_user_sessions feature is disabled");
+
+ RealmResource realm = this.realm.admin();
+
+ ClientResource client = managedClient.admin();
+ ClientRepresentation clientRepresentation = client.toRepresentation();
+
+ // Duplicate the primary client to have two clients to test with
+ ClientRepresentation clientRepresentation2 = client.toRepresentation();
+ clientRepresentation2.setClientId("test-app2");
+ clientRepresentation2.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, "500");
+ clientRepresentation2.setId(null);
+ String clientUUID;
+ try (Response resp = realm.clients().create(clientRepresentation2)) {
+ clientUUID = ApiUtil.getCreatedId(resp);
+ }
+
+ String origClientId = oauth.getClientId();
+ String origClientSecret = oauth.config().getClientSecret();
+
+ try {
+ oauth.doLogin("test-user@localhost", "password");
+
+ EventRepresentation loginEvent = events.poll();
+ EventAssertion.assertSuccess(loginEvent)
+ .type(EventType.LOGIN);
+ String sessionId = loginEvent.getSessionId();
+
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
+
+ // Reduce the idle time so that the originally issued refresh token is valid, but it will be considered invalid due to the client configuration
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, "500");
+ client.update(clientRepresentation);
+
+ oauth.client("test-app2", origClientSecret);
+
+ // We are already logged in due to the token
+ oauth.openLoginForm();
+
+ String code2 = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse tokenResponse2 = oauth.doAccessTokenRequest(code2);
+
+ assertThat(sessionId, Matchers.equalTo(tokenResponse2.getSessionState()));
+
+ timeOffSet.set(100);
+
+ tokenResponse2 = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken());
+ assertEquals(200, tokenResponse2.getStatusCode());
+ assertTrue(0 < tokenResponse2.getRefreshExpiresIn() && tokenResponse2.getRefreshExpiresIn() <= 500, "Invalid RefreshExpiresIn: " + tokenResponse2.getRefreshExpiresIn());
+
+ // Clear all entries from the cache to enforce re-loading the data from the database
+ runOnServer.run(session -> {
+ InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
+ if (connections != null) {
+ Cache> sessionCache = connections.getCache(USER_SESSION_CACHE_NAME);
+ Cache> clientSessionCache = connections.getCache(CLIENT_SESSION_CACHE_NAME);
+ if (sessionCache != null) {
+ sessionCache.clear();
+ }
+ if (clientSessionCache != null) {
+ clientSessionCache.clear();
+ }
+ }
+ });
+
+ timeOffSet.set(550);
+ oauth.client(origClientId, origClientSecret);
+ events.poll();
+
+ // The client session of the first client should have expired by now
+ tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
+
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertNull(tokenResponse.getAccessToken());
+ assertNull(tokenResponse.getRefreshToken());
+ expectRefreshEventError();
+ } finally {
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, "");
+ client.update(clientRepresentation);
+ Response ignored = realm.clients().delete(clientUUID);
+ ignored.close();
+ }
+ }
+
+
+ @Test
+ public void testClientSessionMaxLifespan() {
+ ClientResource client = managedClient.admin();
+ ClientRepresentation clientRepresentation = client.toRepresentation();
+
+ RealmResource realm = this.realm.admin();
+ RealmRepresentation rep = realm.toRepresentation();
+ Integer originalSsoSessionMaxLifespan = rep.getSsoSessionMaxLifespan();
+ int ssoSessionMaxLifespan = rep.getSsoSessionIdleTimeout() - 100;
+ Integer originalClientSessionMaxLifespan = rep.getClientSessionMaxLifespan();
+
+ try {
+ rep.setSsoSessionMaxLifespan(ssoSessionMaxLifespan);
+ realm.update(rep);
+
+ oauth.doLogin("test-user@localhost", "password");
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ assertEquals(200, response.getStatusCode());
+ Assert.assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan);
+
+ rep.setClientSessionMaxLifespan(ssoSessionMaxLifespan - 100);
+ realm.update(rep);
+
+ String refreshToken = response.getRefreshToken();
+ response = oauth.doRefreshTokenRequest(refreshToken);
+ assertEquals(200, response.getStatusCode());
+ Assert.assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan - 100);
+
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_MAX_LIFESPAN,
+ Integer.toString(ssoSessionMaxLifespan - 200));
+ client.update(clientRepresentation);
+
+ refreshToken = response.getRefreshToken();
+ response = oauth.doRefreshTokenRequest(refreshToken);
+ assertEquals(200, response.getStatusCode());
+ Assert.assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan - 200);
+ } finally {
+ rep.setSsoSessionMaxLifespan(originalSsoSessionMaxLifespan);
+ rep.setClientSessionMaxLifespan(originalClientSessionMaxLifespan);
+ realm.update(rep);
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_MAX_LIFESPAN, "");
+ client.update(clientRepresentation);
+ }
+ }
+
+ @Test
+ public void testClientSessionIdleTimeout() {
+ ClientResource client = managedClient.admin();
+ ClientRepresentation clientRepresentation = client.toRepresentation();
+
+ RealmResource realm = this.realm.admin();
+ RealmRepresentation rep = realm.toRepresentation();
+ int ssoSessionIdleTimeout = rep.getSsoSessionIdleTimeout();
+ Integer originalClientSessionIdleTimeout = rep.getClientSessionIdleTimeout();
+
+ try {
+ oauth.doLogin("test-user@localhost", "password");
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ assertEquals(200, response.getStatusCode());
+ Assert.assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout);
+
+ rep.setClientSessionIdleTimeout(ssoSessionIdleTimeout - 100);
+ realm.update(rep);
+
+ String refreshToken = response.getRefreshToken();
+ response = oauth.doRefreshTokenRequest(refreshToken);
+ assertEquals(200, response.getStatusCode());
+ Assert.assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout - 100);
+
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT,
+ Integer.toString(ssoSessionIdleTimeout - 200));
+ client.update(clientRepresentation);
+
+ refreshToken = response.getRefreshToken();
+ response = oauth.doRefreshTokenRequest(refreshToken);
+ assertEquals(200, response.getStatusCode());
+ Assert.assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout - 200);
+ } finally {
+ rep.setClientSessionIdleTimeout(originalClientSessionIdleTimeout);
+ realm.update(rep);
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, "");
+ client.update(clientRepresentation);
+ }
+ }
+
+ @Test // KEYCLOAK-17323
+ public void testRefreshTokenWhenClientSessionTimeoutPassedButRealmDidNot() {
+ realm.updateWithCleanup(r -> r
+ .ssoSessionIdleTimeout(2592000) // 30 Days
+ .ssoSessionMaxLifespan(86313600) // 999 Days
+ );
+
+ ClientResource client = managedClient.admin();
+ ClientRepresentation clientRepresentation = client.toRepresentation();
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, "60"); // 1 minute
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_MAX_LIFESPAN, "65"); // 1 minute 5 seconds
+ client.update(clientRepresentation);
+
+ try {
+ oauth.doLogin("test-user@localhost", "password");
+ String code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code);
+ assertEquals(200, response.getStatusCode());
+ Assert.assertExpiration(response.getExpiresIn(), 65);
+
+ timeOffSet.set(70);
+
+ oauth.openLoginForm();
+ code = oauth.parseLoginResponse().getCode();
+ AccessTokenResponse response2 = oauth.doAccessTokenRequest(code);
+ Assert.assertExpiration(response2.getExpiresIn(), 65);
+ } finally {
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, "");
+ clientRepresentation.getAttributes().put(CLIENT_SESSION_MAX_LIFESPAN, "");
+ client.update(clientRepresentation);
+ }
+ }
+
+
+}
diff --git a/tests/base/src/test/java/org/keycloak/tests/suites/ClusterlessTestSuite.java b/tests/base/src/test/java/org/keycloak/tests/suites/ClusterlessTestSuite.java
index e0a83a17c27..cde5d8f22b6 100644
--- a/tests/base/src/test/java/org/keycloak/tests/suites/ClusterlessTestSuite.java
+++ b/tests/base/src/test/java/org/keycloak/tests/suites/ClusterlessTestSuite.java
@@ -8,6 +8,7 @@ import org.keycloak.tests.admin.client.SessionTest;
import org.keycloak.tests.admin.concurrency.ConcurrentLoginTest;
import org.keycloak.tests.model.UserSessionProviderOfflineTest;
import org.keycloak.tests.model.UserSessionProviderTest;
+import org.keycloak.tests.oauth.RefreshTokenTimeoutsTest;
import org.junit.platform.suite.api.AfterSuite;
import org.junit.platform.suite.api.BeforeSuite;
@@ -15,7 +16,13 @@ import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
@Suite
-@SelectClasses({SessionTest.class, ConcurrentLoginTest.class, UserSessionProviderTest.class, UserSessionProviderOfflineTest.class})
+@SelectClasses({
+ SessionTest.class,
+ ConcurrentLoginTest.class,
+ RefreshTokenTimeoutsTest.class,
+ UserSessionProviderTest.class,
+ UserSessionProviderOfflineTest.class
+})
public class ClusterlessTestSuite {
@BeforeSuite
diff --git a/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java b/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java
index eb4165713f6..26409cb978e 100644
--- a/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java
+++ b/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java
@@ -1,6 +1,7 @@
package org.keycloak.tests.suites;
import org.keycloak.tests.keys.GeneratedRsaKeyProviderTest;
+import org.keycloak.tests.oauth.RefreshTokenTimeoutsTest;
import org.keycloak.tests.transactions.TransactionsTest;
import org.junit.platform.suite.api.SelectClasses;
@@ -14,6 +15,7 @@ import org.junit.platform.suite.api.Suite;
})
@SelectClasses({
GeneratedRsaKeyProviderTest.class,
+ RefreshTokenTimeoutsTest.class,
TransactionsTest.class
})
public class DatabaseTestSuite {
diff --git a/tests/base/src/test/java/org/keycloak/tests/suites/MultisiteTestSuite.java b/tests/base/src/test/java/org/keycloak/tests/suites/MultisiteTestSuite.java
index 81fa826b88d..c13deaa371d 100644
--- a/tests/base/src/test/java/org/keycloak/tests/suites/MultisiteTestSuite.java
+++ b/tests/base/src/test/java/org/keycloak/tests/suites/MultisiteTestSuite.java
@@ -8,6 +8,7 @@ import org.keycloak.tests.admin.client.SessionTest;
import org.keycloak.tests.admin.concurrency.ConcurrentLoginTest;
import org.keycloak.tests.model.UserSessionProviderOfflineTest;
import org.keycloak.tests.model.UserSessionProviderTest;
+import org.keycloak.tests.oauth.RefreshTokenTimeoutsTest;
import org.junit.platform.suite.api.AfterSuite;
import org.junit.platform.suite.api.BeforeSuite;
@@ -15,7 +16,13 @@ import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
@Suite
-@SelectClasses({SessionTest.class, ConcurrentLoginTest.class, UserSessionProviderTest.class, UserSessionProviderOfflineTest.class})
+@SelectClasses({
+ SessionTest.class,
+ ConcurrentLoginTest.class,
+ RefreshTokenTimeoutsTest.class,
+ UserSessionProviderTest.class,
+ UserSessionProviderOfflineTest.class,
+})
public class MultisiteTestSuite {
@BeforeSuite
diff --git a/tests/base/src/test/java/org/keycloak/tests/suites/VolatileSessionsTestSuite.java b/tests/base/src/test/java/org/keycloak/tests/suites/VolatileSessionsTestSuite.java
index 1364b86d7e9..da48dfa026a 100644
--- a/tests/base/src/test/java/org/keycloak/tests/suites/VolatileSessionsTestSuite.java
+++ b/tests/base/src/test/java/org/keycloak/tests/suites/VolatileSessionsTestSuite.java
@@ -8,6 +8,7 @@ import org.keycloak.tests.admin.client.SessionTest;
import org.keycloak.tests.admin.concurrency.ConcurrentLoginTest;
import org.keycloak.tests.model.UserSessionProviderOfflineTest;
import org.keycloak.tests.model.UserSessionProviderTest;
+import org.keycloak.tests.oauth.RefreshTokenTimeoutsTest;
import org.junit.platform.suite.api.AfterSuite;
import org.junit.platform.suite.api.BeforeSuite;
@@ -15,7 +16,13 @@ import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
@Suite
-@SelectClasses({SessionTest.class, ConcurrentLoginTest.class, UserSessionProviderTest.class, UserSessionProviderOfflineTest.class})
+@SelectClasses({
+ SessionTest.class,
+ ConcurrentLoginTest.class,
+ RefreshTokenTimeoutsTest.class,
+ UserSessionProviderTest.class,
+ UserSessionProviderOfflineTest.class,
+})
public class VolatileSessionsTestSuite {
@BeforeSuite
diff --git a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AbstractHttpResponse.java b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AbstractHttpResponse.java
index 50321b6a464..445b2a09b9d 100644
--- a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AbstractHttpResponse.java
+++ b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/AbstractHttpResponse.java
@@ -72,6 +72,10 @@ public abstract class AbstractHttpResponse {
return errorDescription;
}
+ public String setErrorDescription(String errorDescription) {
+ return errorDescription;
+ }
+
protected String getContentType() {
Header[] contentTypeHeaders = response.getHeaders("Content-Type");
return contentTypeHeaders != null && contentTypeHeaders.length > 0 ? contentTypeHeaders[0].getValue() : null;
diff --git a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/UserInfoResponse.java b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/UserInfoResponse.java
index e656d2ef118..1ef200ea04f 100644
--- a/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/UserInfoResponse.java
+++ b/tests/utils-shared/src/main/java/org/keycloak/testsuite/util/oauth/UserInfoResponse.java
@@ -1,17 +1,35 @@
package org.keycloak.testsuite.util.oauth;
import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.keycloak.representations.UserInfo;
import org.apache.http.client.methods.CloseableHttpResponse;
+import static org.apache.http.HttpHeaders.WWW_AUTHENTICATE;
+
public class UserInfoResponse extends AbstractHttpResponse {
private UserInfo userInfo;
public UserInfoResponse(CloseableHttpResponse response) throws IOException {
super(response);
+ if (!isSuccess() && !isJson()) {
+ // Error and error_description inside WWW-Authenticate HTTP header. See OIDC specification, section 5.3.3
+ String wwwAuthenticate = getHeader(WWW_AUTHENTICATE);
+ if (wwwAuthenticate != null) {
+ Matcher errorMatcher = Pattern.compile("error=\"(.*?)\"").matcher(wwwAuthenticate);
+ if (errorMatcher.find()) {
+ setError(errorMatcher.group(1));
+ }
+ Matcher errorDescMatcher = Pattern.compile("error_description=\"(.*?)\"").matcher(wwwAuthenticate);
+ if (errorDescMatcher.find()) {
+ setErrorDescription(errorDescMatcher.group(1));
+ }
+ }
+ }
}
@Override
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
index 10c1468f522..bd7158b951e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
@@ -53,6 +53,8 @@ import static org.junit.Assert.assertTrue;
*/
public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
+ public static final int ALLOWED_CLOCK_SKEW = 3;
+
@Rule
public AssertEvents events = new AssertEvents(this);
@@ -436,7 +438,7 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
Assert.assertNotNull(refreshTokenString);
assertThat(token.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(200L), lessThanOrEqualTo(350L)));
long actual = refreshToken.getExp() - getCurrentTime();
- assertThat(actual, allOf(greaterThanOrEqualTo(1799L - RefreshTokenTest.ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + RefreshTokenTest.ALLOWED_CLOCK_SKEW)));
+ assertThat(actual, allOf(greaterThanOrEqualTo(1799L - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + ALLOWED_CLOCK_SKEW)));
assertEquals(sessionId, refreshToken.getSessionState());
setTimeOffset(2);
@@ -451,7 +453,7 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
assertThat(refreshResponse.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
- assertThat(refreshedToken.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(250L - RefreshTokenTest.ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(300L + RefreshTokenTest.ALLOWED_CLOCK_SKEW)));
+ assertThat(refreshedToken.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(250L - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(300L + ALLOWED_CLOCK_SKEW)));
assertThat(refreshedToken.getExp() - token.getExp(), allOf(greaterThanOrEqualTo(1L), lessThanOrEqualTo(10L)));
assertThat(refreshedRefreshToken.getExp() - refreshToken.getExp(), allOf(greaterThanOrEqualTo(1L), lessThanOrEqualTo(10L)));
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
deleted file mode 100755
index 7789416031b..00000000000
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ /dev/null
@@ -1,2350 +0,0 @@
-/*
- * Copyright 2016 Red Hat, Inc. and/or its affiliates
- * and other contributors as indicated by the @author tags.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.keycloak.testsuite.oauth;
-
-import java.io.Closeable;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import jakarta.ws.rs.client.Client;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.client.WebTarget;
-import jakarta.ws.rs.core.Form;
-import jakarta.ws.rs.core.HttpHeaders;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.UriBuilder;
-
-import org.keycloak.OAuth2Constants;
-import org.keycloak.OAuthErrorException;
-import org.keycloak.admin.client.resource.ClientResource;
-import org.keycloak.admin.client.resource.RealmResource;
-import org.keycloak.admin.client.resource.RealmsResource;
-import org.keycloak.admin.client.resource.UserResource;
-import org.keycloak.common.Profile;
-import org.keycloak.common.enums.SslRequired;
-import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
-import org.keycloak.cookie.CookieType;
-import org.keycloak.crypto.Algorithm;
-import org.keycloak.events.Details;
-import org.keycloak.events.Errors;
-import org.keycloak.events.EventType;
-import org.keycloak.jose.jws.JWSHeader;
-import org.keycloak.jose.jws.JWSInput;
-import org.keycloak.models.AuthenticatedClientSessionModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.models.sessions.infinispan.changes.SessionEntityWrapper;
-import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity;
-import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
-import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.models.utils.SessionTimeoutHelper;
-import org.keycloak.protocol.oidc.OIDCConfigAttributes;
-import org.keycloak.protocol.oidc.OIDCLoginProtocol;
-import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
-import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
-import org.keycloak.representations.AccessToken;
-import org.keycloak.representations.IDToken;
-import org.keycloak.representations.RefreshToken;
-import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.ClientScopeRepresentation;
-import org.keycloak.representations.idm.EventRepresentation;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.UserSessionRepresentation;
-import org.keycloak.testsuite.AbstractKeycloakTest;
-import org.keycloak.testsuite.AssertEvents;
-import org.keycloak.testsuite.ProfileAssume;
-import org.keycloak.testsuite.admin.ApiUtil;
-import org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancer;
-import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
-import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
-import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
-import org.keycloak.testsuite.util.AdminClientUtil;
-import org.keycloak.testsuite.util.BrowserTabUtil;
-import org.keycloak.testsuite.util.ClientManager;
-import org.keycloak.testsuite.util.RealmBuilder;
-import org.keycloak.testsuite.util.RealmManager;
-import org.keycloak.testsuite.util.TokenSignatureUtil;
-import org.keycloak.testsuite.util.UserBuilder;
-import org.keycloak.testsuite.util.UserInfoClientUtil;
-import org.keycloak.testsuite.util.UserManager;
-import org.keycloak.testsuite.util.WaitUtils;
-import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
-import org.keycloak.util.BasicAuthHelper;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import org.hamcrest.CoreMatchers;
-import org.hamcrest.Matchers;
-import org.infinispan.Cache;
-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.openqa.selenium.Cookie;
-
-import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME;
-import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME;
-import static org.keycloak.protocol.oidc.OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT;
-import static org.keycloak.protocol.oidc.OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN;
-import static org.keycloak.testsuite.AbstractAdminTest.loadJson;
-import static org.keycloak.testsuite.Assert.assertExpiration;
-import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
-import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getHttpAuthServerContextRoot;
-import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
-import static org.keycloak.testsuite.util.oauth.OAuthClient.AUTH_SERVER_ROOT;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.greaterThan;
-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.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-/**
- * @author Stian Thorgersen
- */
-public class RefreshTokenTest extends AbstractKeycloakTest {
-
- public static final int ALLOWED_CLOCK_SKEW = 3;
-
- @Page
- protected LoginPage loginPage;
-
- @Rule
- public AssertEvents events = new AssertEvents(this);
-
- @Override
- public void beforeAbstractKeycloakTest() throws Exception {
- super.beforeAbstractKeycloakTest();
- }
-
- @Before
- public void clientConfiguration() {
- ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true);
- }
-
- @Override
- public void addTestRealms(List testRealms) {
-
- RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
-
- realmRepresentation.getClients().add(org.keycloak.testsuite.util.ClientBuilder.create()
- .clientId("service-account-app")
- .serviceAccount()
- .attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "true")
- .secret("secret")
- .build());
-
- RealmBuilder realm = RealmBuilder.edit(realmRepresentation)
- .testEventListener();
-
- testRealms.add(realm.build());
-
- }
-
-
- /**
- * KEYCLOAK-547
- *
- */
- @Test
- public void nullRefreshToken() {
- Client client = AdminClientUtil.createResteasyClient();
- UriBuilder builder = UriBuilder.fromUri(AUTH_SERVER_ROOT);
- URI uri = OIDCLoginProtocolService.tokenUrl(builder).build("test");
- WebTarget target = client.target(uri);
-
- String header = BasicAuthHelper.createHeader("test-app", "password");
- Form form = new Form();
- Response response = target.request()
- .header(HttpHeaders.AUTHORIZATION, header)
- .post(Entity.form(form));
- assertEquals(400, response.getStatus());
- response.close();
- events.clear();
- }
-
- @Test
- public void invalidRefreshToken() {
- AccessTokenResponse response = oauth.doRefreshTokenRequest("invalid");
- assertEquals(400, response.getStatusCode());
- assertEquals("invalid_grant", response.getError());
- events.clear();
- }
-
- @Test
- public void refreshTokenStructure() {
- oauth.loginForm().nonce("123456").doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
- assertNull(token.getNonce());
-
- IDToken idToken = oauth.verifyToken(tokenResponse.getIdToken());
- assertEquals("123456", idToken.getNonce());
-
- String refreshTokenString = tokenResponse.getRefreshToken();
- RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- assertNotNull(refreshTokenString);
-
- assertNull(refreshToken.getNonce());
- assertNull("RealmAccess should be null for RefreshTokens", refreshToken.getRealmAccess());
- assertTrue("ResourceAccess should be null for RefreshTokens", refreshToken.getResourceAccess().isEmpty());
- }
-
- @Test
- public void refreshTokenRequest() {
- oauth.loginForm().nonce("123456").doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
- assertNull(token.getNonce());
-
- IDToken idToken = oauth.verifyToken(tokenResponse.getIdToken(), IDToken.class);
- assertEquals("123456", idToken.getNonce());
-
- assertNotNull(tokenResponse.getRefreshToken());
- RefreshToken refreshToken = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
-
- EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- assertEquals("Bearer", tokenResponse.getTokenType());
-
- assertThat(token.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(200L), lessThanOrEqualTo(350L)));
- long actual = refreshToken.getExp() - getCurrentTime();
- assertThat(actual, allOf(greaterThanOrEqualTo(1799L - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + ALLOWED_CLOCK_SKEW)));
-
- assertEquals(sessionId, refreshToken.getSessionId());
- assertNull(refreshToken.getNonce());
-
- AccessTokenResponse response = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
- RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(response.getRefreshToken());
-
- assertEquals(200, response.getStatusCode());
-
- assertEquals(sessionId, refreshedToken.getSessionId());
- assertEquals(sessionId, refreshedRefreshToken.getSessionId());
-
- assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
- assertThat(refreshedToken.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(250L - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(300L + ALLOWED_CLOCK_SKEW)));
-
- assertThat(refreshedToken.getExp() - token.getExp(), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(10L)));
- assertThat(refreshedRefreshToken.getExp() - refreshToken.getExp(), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(10L)));
-
- // "test-app" should not be an audience in the refresh token
- assertEquals("test-app", refreshedRefreshToken.getIssuedFor());
- Assert.assertFalse(refreshedRefreshToken.hasAudience("test-app"));
-
- Assert.assertNotEquals(token.getId(), refreshedToken.getId());
- Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId());
-
- assertEquals("Bearer", response.getTokenType());
-
- assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), refreshedToken.getSubject());
- // The following check is not valid anymore since file store does have the same ID, and is redundant due to the previous line
- // Assert.assertNotEquals("test-user@localhost", refreshedToken.getSubject());
-
- assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
-
- assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size());
- assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
-
- EventRepresentation refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent();
- Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
- Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
-
- assertNull(refreshedToken.getNonce());
-
- idToken = oauth.verifyToken(response.getIdToken(), IDToken.class);
- assertNull(idToken.getNonce()); // null after refresh as recommended by spec
-
- assertNotNull(response.getRefreshToken());
- refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
- assertEquals(sessionId, refreshToken.getSessionId());
- assertNull(refreshToken.getNonce());
- }
-
- @Test
- public void refreshTokenWithDifferentIssuer() {
- final String proxyHost = "localhost";
- final int httpPort = 8666;
- final int httpsPort = 8667;
-
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- String refreshTokenString = response.getRefreshToken();
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- SimpleUndertowLoadBalancer proxy = new SimpleUndertowLoadBalancer(proxyHost, httpPort, httpsPort, "node1=" + getHttpAuthServerContextRoot() + "/auth");
- proxy.start();
-
- try {
- oauth.baseUrl(String.format("http://%s:%s", proxyHost, httpPort));
-
- response = oauth.doRefreshTokenRequest(refreshTokenString);
-
- Assert.assertEquals(400, response.getStatusCode());
- Assert.assertEquals("invalid_grant", response.getError());
- assertThat(response.getErrorDescription(), Matchers.startsWith("Invalid token issuer."));
- events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
- } finally {
- proxy.stop();
- oauth.baseUrl(AUTH_SERVER_ROOT);
- }
- }
-
-
- @Test
- public void refreshingTokenLoadsSessionIntoCache() {
-
- ProfileAssume.assumeFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS);
-
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- String refreshTokenString = response.getRefreshToken();
-
- // Test when neither client nor user session is in the cache
- testingClient.server().run(session -> {
- session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).clear();
- session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).clear();
- });
-
- response = oauth.doRefreshTokenRequest(refreshTokenString);
- Assert.assertEquals(200, response.getStatusCode());
-
- testingClient.server().run(session -> {
- assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).size(),
- greaterThan(0));
- assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).size(),
- greaterThan(0));
- });
-
- // Test is only the client session is missing
- testingClient.server().run(session -> session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).clear());
-
- response = oauth.doRefreshTokenRequest(refreshTokenString);
- Assert.assertEquals(200, response.getStatusCode());
-
- testingClient.server().run(session -> {
- assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(USER_SESSION_CACHE_NAME).size(),
- greaterThan(0));
- assertThat(session.getProvider(InfinispanConnectionProvider.class).getCache(CLIENT_SESSION_CACHE_NAME).size(),
- greaterThan(0));
- });
-
- }
-
- @Test
- public void refreshTokenWithAccessToken() {
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- String accessTokenString = tokenResponse.getAccessToken();
-
- AccessTokenResponse response = oauth.doRefreshTokenRequest(accessTokenString);
- Assert.assertNotEquals(200, response.getStatusCode());
- }
-
- @Test
- public void testDoNotResolveOfflineUserSessionIfAuthenticationSessionIsInvalidated() {
- oauth.scope("offline_access");
- testDoNotResolveUserSessionIfAuthenticationSessionIsInvalidated();
- }
-
- @Test
- public void testDoNotResolveUserSessionIfAuthenticationSessionIsInvalidated() {
- String realmName = KeycloakModelUtils.generateId();
- RealmsResource realmsResource = realmsResouce();
- realmsResource.create(RealmBuilder.create().name(realmName).build());
- RealmResource realmResource = realmsResource.realm(realmName);
- RealmRepresentation realm = realmResource.toRepresentation();
-
- try {
- realm.setSsoSessionMaxLifespan((int) TimeUnit.MINUTES.toSeconds(2));
- realm.setSsoSessionIdleTimeout((int) TimeUnit.MINUTES.toSeconds(2));
- realm.setAccessTokenLifespan((int) TimeUnit.MINUTES.toSeconds(1));
- realmResource.update(realm);
-
- realmResource.clients().create(org.keycloak.testsuite.util.ClientBuilder.create()
- .clientId("public-client")
- .redirectUris("*")
- .publicClient()
- .build()).close();
-
- realmResource.users()
- .create(UserBuilder.create().username("alice")
- .firstName("alice")
- .lastName("alice")
- .email("alice@keycloak.org")
- .password("alice").addRoles("offline_access").build()).close();
- realmResource.users()
- .create(UserBuilder.create().username("bob")
- .firstName("bob")
- .lastName("bob")
- .email("bob@keycloak.org")
- .password("bob").addRoles("offline_access").build()).close();
-
- oauth.realm(realmName);
- oauth.client("public-client");
-
- oauth.doLogin("alice", "alice");
- String aliceCode = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(aliceCode);
- AccessToken aliceAt = oauth.verifyToken(tokenResponse.getAccessToken());
-
- setTimeOffset((int) TimeUnit.MINUTES.toSeconds(2));
-
- oauth.doLogin("bob", "bob");
- String bobCode = oauth.parseLoginResponse().getCode();
-
- assertNotEquals(aliceCode, bobCode);
-
- tokenResponse = oauth.doAccessTokenRequest(bobCode);
- String refreshToken = tokenResponse.getRefreshToken();
- tokenResponse = oauth.doRefreshTokenRequest(refreshToken);
- AccessToken bobAt = oauth.verifyToken(tokenResponse.getAccessToken());
-
- assertNotEquals(aliceAt.getSessionId(), bobAt.getSessionId());
- assertEquals("bob", bobAt.getPreferredUsername());
- } finally {
- setTimeOffset(0);
-
- realm.setSsoSessionMaxLifespan(null);
- realm.setSsoSessionIdleTimeout(null);
- realm.setAccessTokenLifespan(null);
- realmResource.update(realm);
- }
- }
-
- @Test
- public void testTimeoutWhenReUsingPreviousAuthenticationSession() {
- String realmName = KeycloakModelUtils.generateId();
- RealmsResource realmsResource = realmsResouce();
- realmsResource.create(RealmBuilder.create().name(realmName).build());
- RealmResource realmResource = realmsResource.realm(realmName);
- RealmRepresentation realm = realmResource.toRepresentation();
-
- try {
- realm.setSsoSessionMaxLifespan((int) TimeUnit.MINUTES.toSeconds(2));
- realm.setSsoSessionIdleTimeout((int) TimeUnit.MINUTES.toSeconds(2));
- realm.setAccessTokenLifespan((int) TimeUnit.MINUTES.toSeconds(1));
- realmResource.update(realm);
-
- realmResource.clients().create(org.keycloak.testsuite.util.ClientBuilder.create()
- .clientId("public-client")
- .redirectUris("*")
- .publicClient()
- .build()).close();
-
- realmResource.users()
- .create(UserBuilder.create().username("alice").password("alice").addRoles("offline_access").build()).close();
- realmResource.users()
- .create(UserBuilder.create().username("bob").password("bob").addRoles("offline_access").build()).close();
-
- oauth.realm(realmName);
- oauth.client("public-client");
-
- oauth.openLoginForm();
-
- Cookie authSessionCookie = driver.manage().getCookieNamed(CookieType.AUTH_SESSION_ID.getName());
-
- oauth.fillLoginForm("alice", "alice");
-
- oauth.parseLoginResponse().getCode();
-// WebClient webClient = DroneHtmlUnitDriver.class.cast(driver).getWebClient();
-// webClient.getCookieManager().clearCookies();
- driver.manage().deleteAllCookies();
- oauth.openLoginForm();
- driver.manage().addCookie(authSessionCookie);
- oauth.fillLoginForm("bob", "bob");
- assertEquals("Your login attempt timed out. Login will start from the beginning.", loginPage.getError());
- } finally {
- setTimeOffset(0);
-
- realm.setSsoSessionMaxLifespan(null);
- realm.setSsoSessionIdleTimeout(null);
- realm.setAccessTokenLifespan(null);
- realmResource.update(realm);
- }
- }
-
- /**
- * KEYCLOAK-15437
- */
- @Test
- public void tokenRefreshWithAccessTokenShouldReturnIdTokenWithAccessTokenHash() {
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- String refreshToken = tokenResponse.getRefreshToken();
-
- AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshToken);
- Assert.assertEquals(200, response.getStatusCode());
- IDToken idToken = oauth.verifyToken(response.getIdToken());
- Assert.assertNotNull("AccessTokenHash should not be null after token refresh", idToken.getAccessTokenHash());
- }
-
- @Test
- public void refreshTokenReuseTokenWithoutRefreshTokensRevoked() {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
- RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
- assertEquals(200, response2.getStatusCode());
-
- events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
-
- AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
-
- assertEquals(200, response3.getStatusCode());
-
- events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
- }
-
-
- @Test
- public void refreshTokenReuseTokenWithoutRefreshTokensRevokedWithLessScopes() {
- //add phone,address as optional scope and request them
- ClientScopeRepresentation phoneScope = adminClient.realm("test").clientScopes().findAll().stream().filter((ClientScopeRepresentation clientScope) ->"phone".equals(clientScope.getName())).findFirst().get();
- ClientScopeRepresentation addressScope = adminClient.realm("test").clientScopes().findAll().stream().filter((ClientScopeRepresentation clientScope) ->"address".equals(clientScope.getName())).findFirst().get();
- ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).addClientScope(phoneScope.getId(),false);
- ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).addClientScope(addressScope.getId(),false);
-
- try {
- oauth.doLogin("test-user@localhost", "password");
-
- oauth.parseLoginResponse().getCode();
-
- String optionalScope = "phone address";
- oauth.scope(optionalScope);
- AccessTokenResponse response1 = oauth.doPasswordGrantRequest("test-user@localhost", "password");
- RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
- AbstractOIDCScopeTest.assertScopes("openid basic email roles web-origins acr profile address phone", refreshToken1.getScope());
-
- setTimeOffset(2);
-
- String scope = "email phone";
- oauth.scope(scope);
- AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
- assertEquals(200, response2.getStatusCode());
- AbstractOIDCScopeTest.assertScopes("openid email phone profile", response2.getScope());
- RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
- assertNotNull(refreshToken2);
- AbstractOIDCScopeTest.assertScopes("openid acr roles phone address email profile basic web-origins", refreshToken2.getScope());
-
- } finally {
- setTimeOffset(0);
- oauth.scope(null);
- }
- }
-
- @Test
- public void refreshTokenReuseTokenScopeParameterNotInRefreshToken() {
- try {
- //scope parameter consists scope that is not part of scope refresh token => error thrown
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
- RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
- AbstractOIDCScopeTest.assertScopes("openid basic email roles web-origins acr profile", refreshToken1.getScope());
-
- setTimeOffset(2);
-
- String scope = "openid email ssh_public_key";
- oauth.scope(scope);
- AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
- assertEquals(400, response2.getStatusCode());
- assertEquals(OAuthErrorException.INVALID_SCOPE, response2.getError());
-
- } finally {
- setTimeOffset(0);
- oauth.scope(null);
- }
- }
-
- @Test
- public void refreshWithOptionalClientScopeWithIncludeInTokenScopeDisabled() {
- //set roles client scope as optional
- ClientScopeRepresentation rolesScope = ApiUtil.findClientScopeByName(adminClient.realm("test"), OIDCLoginProtocolFactory.ROLES_SCOPE).toRepresentation();
- ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).removeClientScope(rolesScope.getId(),true);
- ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).addClientScope(rolesScope.getId(),false);
-
- try {
- oauth.scope("roles");
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
- RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
-
- AbstractOIDCScopeTest.assertScopes("openid email profile", accessToken.getScope());
- AbstractOIDCScopeTest.assertScopes("openid basic email roles web-origins acr profile", refreshToken.getScope());
-
- Assert.assertNotNull(accessToken.getRealmAccess());
- Assert.assertNotNull(accessToken.getResourceAccess());
-
- oauth.scope(null);
-
- response = oauth.doRefreshTokenRequest(response.getRefreshToken());
-
- accessToken = oauth.verifyToken(response.getAccessToken());
- refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
-
- AbstractOIDCScopeTest.assertScopes("openid email profile", accessToken.getScope());
- AbstractOIDCScopeTest.assertScopes("openid basic email roles web-origins acr profile", refreshToken.getScope());
-
- Assert.assertNotNull(accessToken.getRealmAccess());
- Assert.assertNotNull(accessToken.getResourceAccess());
-
- } finally {
- ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).removeClientScope(rolesScope.getId(),false);
- ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).addClientScope(rolesScope.getId(),true);
- }
- }
-
- @Test
- public void refreshTokenReuseTokenWithRefreshTokensRevoked() {
- try {
-
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true);
-
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
- RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
- RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
-
- assertEquals(200, response2.getStatusCode());
-
- events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
-
- AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
-
- assertEquals(400, response3.getStatusCode());
-
- events.expectRefresh(refreshToken1.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
-
- // Client session invalidated hence old refresh token not valid anymore
- AccessTokenResponse response4 = oauth.doRefreshTokenRequest(response2.getRefreshToken());
- assertEquals(400, response4.getStatusCode());
- events.expectRefresh(refreshToken2.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
- } finally {
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
- }
- }
-
- // GH issue 45647
- @Test
- public void refreshTokenRevokeWithConcurrentRefreshTokenRequests() {
- try {
- RealmManager.realm(adminClient.realm("test"))
- .revokeRefreshToken(true);
-
- testRefreshTokenConcurrentReuse(1);
- } finally {
- RealmManager.realm(adminClient.realm("test"))
- .revokeRefreshToken(false);
- }
- }
-
- // GH issue 45647
- @Test
- public void refreshTokenRevokeAndReuseWithConcurrentRefreshTokenRequests() {
- try {
- RealmManager.realm(adminClient.realm("test"))
- .refreshTokenMaxReuse(2)
- .revokeRefreshToken(true);
-
- testRefreshTokenConcurrentReuse(3);
- } finally {
- RealmManager.realm(adminClient.realm("test"))
- .refreshTokenMaxReuse(0)
- .revokeRefreshToken(false);
- }
- }
-
- private void testRefreshTokenConcurrentReuse(int expectedSuccessfulRefreshes) {
- int THREADS_COUNT = 5;
-
- oauth.doLogin("test-user@localhost", "password");
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
-
- // Try concurrent requests for refresh-token requests
- AtomicInteger successCounts = new AtomicInteger(0);
- AtomicInteger errorCounts = new AtomicInteger(0);
- Runnable runnable = () -> {
- AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
- if (response2.getStatusCode() == 200) {
- successCounts.incrementAndGet();
- } else {
- assertEquals(400, response2.getStatusCode());
- errorCounts.incrementAndGet();
- }
- };
-
- List threads = new ArrayList<>();
- for (int i = 0 ; i < THREADS_COUNT ; i++) {
- threads.add(new Thread(runnable));
- }
- for (Thread t : threads) {
- t.start();
- }
- for (Thread t : threads) {
- try {
- t.join();
- } catch(InterruptedException ie) {
- Assert.fail("Interrupted exception thrown in one of the threads during token refresh");
- }
- }
-
- // Check expected successful count of refreshes
- assertEquals("Expected only " + expectedSuccessfulRefreshes + " successful refreshes, but was successfully refreshed " + successCounts.get() + " times", expectedSuccessfulRefreshes, successCounts.get());
- assertEquals(THREADS_COUNT - expectedSuccessfulRefreshes, errorCounts.get());
- }
-
- @Test
- public void refreshTokenReuseOnDifferentTab() {
- try {
-
- BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver);
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true);
-
- //login with tab 1
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
- RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
- events.expectCodeToToken(codeId, sessionId).assertEvent();
- assertNotNull(refreshToken1.getOtherClaims().get(Constants.REUSE_ID));
- assertNotEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshToken1.getId());
-
- //login with tab 2
- tabUtil.newTab(oauth.loginForm().build());
- assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
-
- loginEvent = events.expectLogin().assertEvent();
- sessionId = loginEvent.getSessionId();
- codeId = loginEvent.getDetails().get(Details.CODE_ID);
- code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse responseNew = oauth.doAccessTokenRequest(code);
- RefreshToken refreshTokenNew = oauth.parseRefreshToken(responseNew.getRefreshToken());
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
- assertNotNull(refreshToken1.getOtherClaims().get(Constants.REUSE_ID));
- assertNotEquals(refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew.getId());
- assertNotEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID));
-
- setTimeOffset(10);
-
- //refresh with token from tab 1
- AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
- assertEquals(200, response2.getStatusCode());
- RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
- events.expectRefresh(refreshToken2.getId(), sessionId);
- assertNotEquals(refreshToken2.getOtherClaims().get(Constants.REUSE_ID), refreshToken2.getId());
- assertEquals(refreshToken1.getOtherClaims().get(Constants.REUSE_ID), refreshToken2.getOtherClaims().get(Constants.REUSE_ID));
-
- //refresh with token from tab 2
- AccessTokenResponse responseNew1 = oauth.doRefreshTokenRequest(responseNew.getRefreshToken());
- assertEquals(200, responseNew1.getStatusCode());
- RefreshToken refreshTokenNew1 = oauth.parseRefreshToken(responseNew1.getRefreshToken());
- events.expectRefresh(refreshTokenNew1.getId(), sessionId);
- assertNotEquals(refreshTokenNew1.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew1.getId());
- assertEquals(refreshTokenNew.getOtherClaims().get(Constants.REUSE_ID), refreshTokenNew1.getOtherClaims().get(Constants.REUSE_ID));
-
- //try refresh token reuse with token from tab 2
- responseNew1 = oauth.doRefreshTokenRequest(responseNew.getRefreshToken());
- assertEquals(400, responseNew1.getStatusCode());
-
-
- } finally {
- resetTimeOffset();
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
- }
- }
-
- @Test
- public void refreshTokenReuseTokenWithRefreshTokensRevokedAfterSingleReuse() {
- try {
- RealmManager.realm(adminClient.realm("test"))
- .revokeRefreshToken(true)
- .refreshTokenMaxReuse(1);
-
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse initialResponse = oauth.doAccessTokenRequest(code);
- RefreshToken initialRefreshToken = oauth.parseRefreshToken(initialResponse.getRefreshToken());
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- // Initial refresh.
- AccessTokenResponse responseFirstUse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
- RefreshToken newTokenFirstUse = oauth.parseRefreshToken(responseFirstUse.getRefreshToken());
-
- assertEquals(200, responseFirstUse.getStatusCode());
-
- events.expectRefresh(initialRefreshToken.getId(), sessionId).assertEvent();
-
- // Second refresh (allowed).
- AccessTokenResponse responseFirstReuse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
- RefreshToken newTokenFirstReuse = oauth.parseRefreshToken(responseFirstReuse.getRefreshToken());
- String userId = newTokenFirstReuse.getSubject();
-
- assertEquals(200, responseFirstReuse.getStatusCode());
-
- events.expectRefresh(initialRefreshToken.getId(), sessionId).detail(Details.REFRESH_TOKEN_SUB, userId).assertEvent();
-
- // Token reused twice, became invalid.
- AccessTokenResponse responseSecondReuse = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
-
- assertEquals(400, responseSecondReuse.getStatusCode());
-
- events.expectRefresh(initialRefreshToken.getId(), sessionId).user((String) null).detail(Details.REFRESH_TOKEN_SUB, userId).removeDetail(Details.TOKEN_ID)
- .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
-
- // Refresh token from first use became invalid.
- AccessTokenResponse responseUseOfInvalidatedRefreshToken =
- oauth.doRefreshTokenRequest(responseFirstUse.getRefreshToken());
-
- assertEquals(400, responseUseOfInvalidatedRefreshToken.getStatusCode());
-
- events.expectRefresh(newTokenFirstUse.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID)
- .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
-
- // Refresh token from reuse is not valid. Client session was invalidated
- AccessTokenResponse responseUseOfValidRefreshToken =
- oauth.doRefreshTokenRequest(responseFirstReuse.getRefreshToken());
-
- assertEquals(400, responseUseOfValidRefreshToken.getStatusCode());
-
- events.expectRefresh(newTokenFirstReuse.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID)
- .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
- } finally {
- RealmManager.realm(adminClient.realm("test"))
- .refreshTokenMaxReuse(0)
- .revokeRefreshToken(false);
- }
- }
-
- @Test
- public void refreshTokenReuseOfExistingTokenAfterEnablingReuseRevokation() {
- try {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse initialResponse = oauth.doAccessTokenRequest(code);
- RefreshToken initialRefreshToken = oauth.parseRefreshToken(initialResponse.getRefreshToken());
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- // Infinite reuse allowed
- processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
- processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
- processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
-
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true).refreshTokenMaxReuse(1);
-
- // Config changed, we start tracking reuse.
- processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
- processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
-
- AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
-
- assertEquals(400, responseReuseExceeded.getStatusCode());
-
- events.expectRefresh(initialRefreshToken.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
- } finally {
- RealmManager.realm(adminClient.realm("test"))
- .refreshTokenMaxReuse(0)
- .revokeRefreshToken(false);
- }
- }
-
- @Test
- public void refreshTokenReuseOfExistingTokenAfterDisablingReuseRevokation() {
- try {
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true).refreshTokenMaxReuse(1);
-
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse initialResponse = oauth.doAccessTokenRequest(code);
- RefreshToken initialRefreshToken = oauth.parseRefreshToken(initialResponse.getRefreshToken());
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- // Single reuse authorized.
- processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
- processExpectedValidRefresh(sessionId, initialRefreshToken, initialResponse.getRefreshToken());
-
- AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
-
- assertEquals(400, responseReuseExceeded.getStatusCode());
-
- events.expectRefresh(initialRefreshToken.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID)
- .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
-
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
-
- // Config changed, token cannot be used again at this point due the client session invalidated
- AccessTokenResponse responseReuseExceeded2 = oauth.doRefreshTokenRequest(initialResponse.getRefreshToken());
- assertEquals(400, responseReuseExceeded2.getStatusCode());
- events.expectRefresh(initialRefreshToken.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID)
- .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
- } finally {
- RealmManager.realm(adminClient.realm("test"))
- .refreshTokenMaxReuse(0)
- .revokeRefreshToken(false);
- }
- }
-
- // Doublecheck that with "revokeRefreshToken" and revoked tokens, the SSO re-authentication won't cause old tokens to be valid again
- @Test
- public void refreshTokenReuseTokenWithRefreshTokensRevokedAndSSOReauthentication() throws Exception {
- try {
- // Initial login
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(true);
-
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response1 = oauth.doAccessTokenRequest(code);
- RefreshToken refreshToken1 = oauth.parseRefreshToken(response1.getRefreshToken());
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- // Refresh token for the first time - should pass
-
- AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
- RefreshToken refreshToken2 = oauth.parseRefreshToken(response2.getRefreshToken());
-
- assertEquals(200, response2.getStatusCode());
-
- events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
-
- // Client sessions is available now
- Assert.assertTrue(hasClientSessionForTestApp());
-
- // Refresh token for the second time - should fail and invalidate client session
-
- AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken());
-
- assertEquals(400, response3.getStatusCode());
-
- events.expectRefresh(refreshToken1.getId(), sessionId).removeDetail(Details.TOKEN_ID).user((String) null).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
-
- // No client sessions available after revoke
- Assert.assertFalse(hasClientSessionForTestApp());
-
- // Introspection with the accessToken from the first authentication. This should fail
- JsonNode jsonNode = oauth.doIntrospectionAccessTokenRequest(response1.getAccessToken()).asJsonNode();
- Assert.assertFalse(jsonNode.get("active").asBoolean());
- events.clear();
-
- // SSO re-authentication
- setTimeOffset(2);
- oauth.openLoginForm();
-
- loginEvent = events.expectLogin().assertEvent();
- sessionId = loginEvent.getSessionId();
- codeId = loginEvent.getDetails().get(Details.CODE_ID);
- code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response4 = oauth.doAccessTokenRequest(code);
- oauth.parseRefreshToken(response4.getRefreshToken());
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- // Client sessions should be available again now after re-authentication
- Assert.assertTrue(hasClientSessionForTestApp());
-
- // Introspection again with the accessToken from the very first authentication. This should fail as the access token was obtained for the old client session before SSO re-authentication
- jsonNode = oauth.doIntrospectionAccessTokenRequest(response1.getAccessToken()).asJsonNode();
- Assert.assertFalse(jsonNode.get("active").asBoolean());
-
- // Try userInfo with the same old access token. Should fail as well
-// UserInfo userInfo = oauth.doUserInfoRequest(response1.getAccessToken());
- Client client = AdminClientUtil.createResteasyClient();
- Response userInfoResponse = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, response1.getAccessToken());
- assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), userInfoResponse.getStatus());
- String wwwAuthHeader = userInfoResponse.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
- assertNotNull(wwwAuthHeader);
- assertThat(wwwAuthHeader, CoreMatchers.containsString("Bearer"));
- assertThat(wwwAuthHeader, CoreMatchers.containsString("error=\"" + OAuthErrorException.INVALID_TOKEN + "\""));
-
- events.clear();
-
- // Try to refresh with one of the old refresh tokens before SSO re-authentication - should fail
- AccessTokenResponse response5 = oauth.doRefreshTokenRequest(response2.getRefreshToken());
- assertEquals(400, response5.getStatusCode());
- events.expectRefresh(refreshToken2.getId(), sessionId).user((String) null).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
- } finally {
- resetTimeOffset();
- RealmManager.realm(adminClient.realm("test")).revokeRefreshToken(false);
- }
- }
-
- // Returns true if "test-user@localhost" has any user session with client session for "test-app"
- private boolean hasClientSessionForTestApp() {
- List userSessions = ApiUtil.findUserByUsernameId(adminClient.realm("test"), "test-user@localhost").getUserSessions();
- return userSessions.stream()
- .anyMatch(userSession -> userSession.getClients().containsValue("test-app"));
- }
-
- private void processExpectedValidRefresh(String sessionId, RefreshToken requestToken, String refreshToken) {
- AccessTokenResponse response2 = oauth.doRefreshTokenRequest(refreshToken);
-
- assertEquals(200, response2.getStatusCode());
-
- events.expectRefresh(requestToken.getId(), sessionId).assertEvent();
- }
-
-
- @Test
- public void refreshTokenClientDisabled() {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- String refreshTokenString = response.getRefreshToken();
- RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- try {
- ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).enabled(false);
-
- response = oauth.doRefreshTokenRequest(refreshTokenString);
-
- assertEquals(401, response.getStatusCode());
- assertEquals("invalid_client", response.getError());
-
- events.expectRefresh(refreshToken.getId(), sessionId).user((String) null).session((String) null).clearDetails().error(Errors.CLIENT_DISABLED).assertEvent();
- } finally {
- ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).enabled(true);
- }
- }
-
- @Test
- public void refreshTokenUserSessionRemoved() {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- events.poll();
-
- String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
-
- testingClient.testing().removeUserSession("test", sessionId);
-
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
-
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
-
- events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
-
- events.clear();
- }
-
- @Test
- public void refreshTokenAfterUserLogoutAndLoginAgain() {
- String refreshToken1 = loginAndForceNewLoginPage();
-
- oauth.doLogout(refreshToken1);
- events.clear();
-
- try {
- // Continue with login
- setTimeOffset(2);
- driver.navigate().refresh();
- oauth.fillLoginForm("test-user@localhost", "password");
-
- assertFalse(loginPage.isCurrent());
-
- AccessTokenResponse tokenResponse2;
- String code = oauth.parseLoginResponse().getCode();
- tokenResponse2 = oauth.doAccessTokenRequest(code);
-
- // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
- AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1);
- assertEquals(400, responseReuseExceeded.getStatusCode());
-
- // Finally try with valid refresh token
- responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken());
- assertEquals(200, responseReuseExceeded.getStatusCode());
- } finally {
- resetTimeOffset();
- }
- }
-
- @Test
- public void refreshTokenAfterAdminLogoutAllAndLoginAgain() {
- String refreshToken1 = loginAndForceNewLoginPage();
-
- adminClient.realm("test").logoutAll();
- // Must wait for server to execute the request. Sometimes, there is issue with the execution and another tests failed, because of this.
- WaitUtils.pause(500);
-
- events.clear();
-
- try {
- // Continue with login
- setTimeOffset(2);
- driver.navigate().refresh();
- oauth.fillLoginForm("test-user@localhost", "password");
-
- assertFalse(loginPage.isCurrent());
-
- AccessTokenResponse tokenResponse2;
- String code = oauth.parseLoginResponse().getCode();
- tokenResponse2 = oauth.doAccessTokenRequest(code);
-
- // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
- AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1);
- assertEquals(400, responseReuseExceeded.getStatusCode());
-
- // Finally try with valid refresh token
- responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken());
- assertEquals(200, responseReuseExceeded.getStatusCode());
- } finally {
- resetTimeOffset();
- }
- }
-
- @Test
- public void refreshTokenAfterUserAdminLogoutEndpointAndLoginAgain() {
- try {
- String refreshToken1 = loginAndForceNewLoginPage();
-
- RefreshToken refreshTokenParsed1 = oauth.parseRefreshToken(refreshToken1);
- String userId = refreshTokenParsed1.getSubject();
- UserResource user = adminClient.realm("test").users().get(userId);
- user.logout();
-
- // Continue with login
- setTimeOffset(2);
- driver.navigate().refresh();
- oauth.fillLoginForm("test-user@localhost", "password");
-
- assertFalse(loginPage.isCurrent());
-
- AccessTokenResponse tokenResponse2;
- String code = oauth.parseLoginResponse().getCode();
- tokenResponse2 = oauth.doAccessTokenRequest(code);
-
- // Now try refresh with the original refreshToken1 created in logged-out userSession. It should fail
- AccessTokenResponse responseReuseExceeded = oauth.doRefreshTokenRequest(refreshToken1);
- assertEquals(400, responseReuseExceeded.getStatusCode());
-
- // Finally try with valid refresh token
- responseReuseExceeded = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken());
- assertEquals(200, responseReuseExceeded.getStatusCode());
- } finally {
- resetTimeOffset();
- // Need to reset not-before of user, which was updated during user.logout()
- testingClient.server().run(session -> {
- RealmModel realm = session.realms().getRealmByName("test");
- UserModel user = session.users().getUserByUsername(realm, "test-user@localhost");
- session.users().setNotBeforeForUser(realm, user, 0);
- });
- }
- }
-
- @Test
- public void testUserSessionRefreshAndIdle() {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- events.poll();
-
- String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
-
- int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
-
- setTimeOffset(2);
-
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
-
- oauth.verifyToken(tokenResponse.getAccessToken());
- oauth.parseRefreshToken(tokenResponse.getRefreshToken());
-
- assertEquals(200, tokenResponse.getStatusCode());
-
- int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
-
- Assert.assertNotEquals(last, next);
-
- RealmResource realmResource = adminClient.realm("test");
- int lastAccessTokenLifespan = realmResource.toRepresentation().getAccessTokenLifespan();
- int originalIdle = realmResource.toRepresentation().getSsoSessionIdleTimeout();
-
- try {
- RealmManager.realm(realmResource).accessTokenLifespan(100000);
-
- setTimeOffset(4);
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
-
- next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
-
- // lastSEssionRefresh should be updated because access code lifespan is higher than sso idle timeout
- assertThat(next, allOf(greaterThan(last), lessThan(last + 50)));
-
- RealmManager.realm(realmResource).ssoSessionIdleTimeout(1);
-
- events.clear();
- // Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
- setTimeOffset(6 + (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
-
- // test idle timeout
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
-
- events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
-
- } finally {
- RealmManager.realm(realmResource).ssoSessionIdleTimeout(originalIdle).accessTokenLifespan(lastAccessTokenLifespan);
- events.clear();
- resetTimeOffset();
- }
-
- }
-
- @Test
- public void testUserSessionRefreshAndIdleRememberMe() throws Exception {
- RealmResource testRealm = adminClient.realm("test");
-
- try (Closeable ignored = new RealmAttributeUpdater(testRealm)
- .updateWith(r -> {
- r.setRememberMe(true);
- r.setSsoSessionIdleTimeoutRememberMe(500);
- r.setSsoSessionIdleTimeout(100);
- }).update()) {
- oauth.openLoginForm();
- loginPage.setRememberMe(true);
- loginPage.login("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- events.poll();
-
- String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
- int last = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
-
- setTimeOffset(110 + (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- oauth.verifyToken(tokenResponse.getAccessToken());
- oauth.parseRefreshToken(tokenResponse.getRefreshToken());
- assertEquals(200, tokenResponse.getStatusCode());
-
- int next = testingClient.testing().getLastSessionRefresh("test", sessionId, false);
- Assert.assertNotEquals(last, next);
-
- events.clear();
- // Needs to add some additional time due the tollerance allowed by IDLE_TIMEOUT_WINDOW_SECONDS
- setTimeOffset(620 + 2 * (ProfileAssume.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS) ? 0 : SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS));
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
-
- // test idle remember me timeout
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
-
- events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
- events.clear();
-
- } finally {
- resetTimeOffset();
- }
- }
-
- private String getClientSessionUuid(final String userSessionId, String clientId) {
- return testingClient.server().fetch(session -> {
- RealmModel realmModel = session.realms().getRealmByName("test");
- ClientModel clientModel = realmModel.getClientByClientId(clientId);
- UserSessionModel userSession = session.sessions().getUserSession(realmModel, userSessionId);
- AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
- return clientSession.getId();
- }, String.class);
- }
-
- private int checkIfUserAndClientSessionExist(final String userSessionId, final String clientId, final String clientSessionId) {
- return testingClient.server().fetch(session -> {
- RealmModel realmModel = session.realms().getRealmByName("test");
- ClientModel clientModel = realmModel.getClientByClientId(clientId);
- UserSessionModel userSession = session.sessions().getUserSession(realmModel, userSessionId);
- if (userSession != null) {
- AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());
- return clientSession != null && clientSessionId.equals(clientSession.getId())? 2 : 1;
- }
- return 0;
- }, Integer.class);
- }
-
- @Test
- public void refreshTokenUserSessionMaxLifespan() throws Exception {
- RealmResource realmResource = adminClient.realm("test");
- getTestingClient().testing().setTestingInfinispanTimeService();
- try (Closeable ignored = new RealmAttributeUpdater(realmResource)
- .updateWith(r -> {
- r.setSsoSessionMaxLifespan(3600);
- r.setSsoSessionIdleTimeout(7200);
- }).update()) {
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3600);
- final String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- events.poll();
-
- setTimeOffset(1800);
-
- String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1800);
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
- events.expectRefresh(refreshId, sessionId).assertEvent();
-
- setTimeOffset(3700);
- oauth.parseRefreshToken(tokenResponse.getRefreshToken());
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
-
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
- events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
- assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
- } finally {
- getTestingClient().testing().revertTestingInfinispanTimeService();
- events.clear();
- resetTimeOffset();
- }
- }
-
- @Test
- public void refreshTokenUserClientMaxLifespanSmallerThanSession() throws Exception {
- RealmResource realmResource = adminClient.realm("test");
- getTestingClient().testing().setTestingInfinispanTimeService();
- try (Closeable ignored = new RealmAttributeUpdater(realmResource)
- .updateWith(r -> {
- r.setSsoSessionMaxLifespan(3600);
- r.setSsoSessionIdleTimeout(7200);
- r.setClientSessionMaxLifespan(1000);
- r.setClientSessionIdleTimeout(7200);
- }).update()) {
-
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1000);
- String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- events.poll();
-
- setTimeOffset(600);
- String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400);
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
- events.expectRefresh(refreshId, sessionId).assertEvent();
-
- setTimeOffset(1100);
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
- events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
- assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- setTimeOffset(1600);
- oauth.openLoginForm();
- loginEvent = events.expectLogin().assertEvent();
- sessionId = loginEvent.getSessionId();
- code = oauth.parseLoginResponse().getCode();
- tokenResponse = oauth.doAccessTokenRequest(code);
- assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 1000);
- events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), sessionId).assertEvent();
-
- clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- setTimeOffset(3700);
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
- events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
- assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
- } finally {
- getTestingClient().testing().revertTestingInfinispanTimeService();
- events.clear();
- resetTimeOffset();
- }
- }
-
- @Test
- public void refreshTokenUserSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
- RealmResource realmResource = adminClient.realm("test");
- getTestingClient().testing().setTestingInfinispanTimeService();
-
- try (Closeable ignored = new RealmAttributeUpdater(realmResource)
- .updateWith(r -> {
- r.setSsoSessionMaxLifespan(7200);
- r.setSsoSessionIdleTimeout(7200);
- r.setClientSessionMaxLifespan(7200);
- r.setClientSessionIdleTimeout(7200);
- }).update()) {
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200);
- final String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- events.poll();
-
- RealmRepresentation rep = realmResource.toRepresentation();
- rep.setSsoSessionMaxLifespan(3600);
- rep.setClientSessionMaxLifespan(3600);
- realmResource.update(rep);
-
- setTimeOffset(3700);
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
- events.assertRefreshTokenErrorAndMaybeSessionExpired(sessionId, loginEvent.getUserId(), loginEvent.getClientId());
- assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
- } finally {
- getTestingClient().testing().revertTestingInfinispanTimeService();
- events.clear();
- resetTimeOffset();
- }
- }
-
- @Test
- public void refreshTokenClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
- RealmResource realmResource = adminClient.realm("test");
- getTestingClient().testing().setTestingInfinispanTimeService();
-
- try (Closeable ignored = new RealmAttributeUpdater(realmResource)
- .updateWith(r -> {
- r.setSsoSessionMaxLifespan(7200);
- r.setSsoSessionIdleTimeout(7200);
- r.setClientSessionMaxLifespan(7200);
- r.setClientSessionIdleTimeout(7200);
- }).update()) {
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- assertEquals(200, tokenResponse.getStatusCode());
- assertTrue("Invalid ExpiresIn: " + tokenResponse.getRefreshExpiresIn(), 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200);
- String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- events.poll();
-
- RealmRepresentation rep = realmResource.toRepresentation();
- rep.setClientSessionMaxLifespan(3600);
- realmResource.update(rep);
-
- setTimeOffset(3700);
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
- events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).session(sessionId).user((String) null).assertEvent();
- assertEquals(1, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- setTimeOffset(4200);
- oauth.openLoginForm();
- loginEvent = events.expectLogin().assertEvent();
- sessionId = loginEvent.getSessionId();
- code = oauth.parseLoginResponse().getCode();
- tokenResponse = oauth.doAccessTokenRequest(code);
- assertEquals(200, tokenResponse.getStatusCode());
- assertTrue("Invalid ExpiresIn: " + tokenResponse.getRefreshExpiresIn(), 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3000);
- events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), sessionId).assertEvent();
-
- clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- setTimeOffset(7300);
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
- events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
- assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
- } finally {
- getTestingClient().testing().revertTestingInfinispanTimeService();
- events.clear();
- resetTimeOffset();
- }
- }
-
- @Test
- public void silentLoginClientSessionMaxLifespanModifiedAfterTokenRefresh() throws Exception {
- RealmResource realmResource = adminClient.realm("test");
- getTestingClient().testing().setTestingInfinispanTimeService();
-
- try (Closeable ignored = new RealmAttributeUpdater(realmResource)
- .updateWith(r -> {
- r.setSsoSessionMaxLifespan(7200);
- r.setSsoSessionIdleTimeout(7200);
- r.setClientSessionMaxLifespan(7200);
- r.setClientSessionIdleTimeout(7200);
- }).update()) {
-
- oauth.doLogin("test-user@localhost", "password");
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
- assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 7200);
- String clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- events.poll();
-
- RealmRepresentation rep = realmResource.toRepresentation();
- rep.setClientSessionMaxLifespan(3600);
- realmResource.update(rep);
-
- setTimeOffset(4200);
- oauth.openLoginForm();
- loginEvent = events.expectLogin().assertEvent();
- sessionId = loginEvent.getSessionId();
- code = oauth.parseLoginResponse().getCode();
- tokenResponse = oauth.doAccessTokenRequest(code);
- assertTrue("Invalid ExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 3000);
- events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), sessionId).assertEvent();
-
- clientSessionId = getClientSessionUuid(sessionId, loginEvent.getClientId());
- assertEquals(2, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
-
- setTimeOffset(7300);
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
- events.expect(EventType.REFRESH_TOKEN).error(Errors.INVALID_TOKEN).user((String) null).assertEvent();
- assertEquals(0, checkIfUserAndClientSessionExist(sessionId, loginEvent.getClientId(), clientSessionId));
- } finally {
- getTestingClient().testing().revertTestingInfinispanTimeService();
- events.clear();
- resetTimeOffset();
- }
- }
-
- /**
- * KEYCLOAK-1267
- * @throws Exception
- */
- @Test
- public void refreshTokenUserSessionMaxLifespanWithRememberMe() throws Exception {
-
- RealmResource testRealm = adminClient.realm("test");
-
- try (Closeable ignored = new RealmAttributeUpdater(testRealm)
- .updateWith(r -> {
- r.setRememberMe(true);
- r.setSsoSessionMaxLifespanRememberMe(100);
- r.setSsoSessionMaxLifespan(50);
- }).update()) {
-
- oauth.openLoginForm();
- loginPage.setRememberMe(true);
- loginPage.login("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- events.poll();
-
- String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
-
- setTimeOffset(110);
-
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
-
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
-
- events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
- events.clear();
-
- } finally {
- resetTimeOffset();
- }
- }
-
- @Test
- public void refreshTokenClientSessionMaxLifespan() {
- RealmResource realm = adminClient.realm("test");
- RealmRepresentation rep = realm.toRepresentation();
- Integer originalSsoSessionMaxLifespan = rep.getSsoSessionMaxLifespan();
-
- ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
- ClientRepresentation clientRepresentation = client.toRepresentation();
-
- getTestingClient().testing().setTestingInfinispanTimeService();
-
- try {
- rep.setSsoSessionMaxLifespan(1000);
- realm.update(rep);
-
- clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, "500");
- client.update(clientRepresentation);
-
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- events.poll();
-
- String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
-
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertTrue("Invalid RefreshExpiresIn" + tokenResponse.getRefreshExpiresIn(), 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 500);
-
- setTimeOffset(100);
-
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertTrue("Invalid RefreshExpiresIn", 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400);
-
- setTimeOffset(600);
-
- oauth.openLoginForm();
- code = oauth.parseLoginResponse().getCode();
-
- tokenResponse = oauth.doAccessTokenRequest(code);
- assertEquals(200, tokenResponse.getStatusCode());
- assertTrue("Invalid RefreshExpiresIn" + tokenResponse.getRefreshExpiresIn(), 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 400);
-
- setTimeOffset(700);
-
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertEquals(200, tokenResponse.getStatusCode());
- assertTrue("Invalid RefreshExpiresIn" + tokenResponse.getRefreshExpiresIn(), 0 < tokenResponse.getRefreshExpiresIn() && tokenResponse.getRefreshExpiresIn() <= 300);
-
- setTimeOffset(1100);
-
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
-
- events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
- } finally {
- rep.setSsoSessionMaxLifespan(originalSsoSessionMaxLifespan);
- realm.update(rep);
- clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, null);
- client.update(clientRepresentation);
-
- events.clear();
- resetTimeOffset();
- getTestingClient().testing().revertTestingInfinispanTimeService();
- }
- }
-
- /**
- * This is a very esoteric test specific to bug #38591.
- * Consider removing or rewriting the test if the loading of sessions from the database has changed and no longer
- * updates the client session timestamp. It is also specific to the case when the idle timeout of a client is reduced
- * while some client sessions already exist.
- */
- @Test
- public void refreshTokenClientSessionIdleTimeoutTwoClientsWithReloadingFromDatabase() {
- ProfileAssume.assumeFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS);
-
- RealmResource realm = adminClient.realm("test");
-
- ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
- ClientRepresentation clientRepresentation = client.toRepresentation();
-
- // Duplicate the primary client to have two clients to test with
- ClientRepresentation clientRepresentation2 = client.toRepresentation();
- clientRepresentation2.setClientId("test-app2");
- clientRepresentation2.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, "500");
- clientRepresentation2.setId(null);
- try (Response resp = realm.clients().create(clientRepresentation2)) {
- String clientUUID = ApiUtil.getCreatedId(resp);
- getCleanup().addClientUuid(clientUUID);
- }
-
- getTestingClient().testing().setTestingInfinispanTimeService();
-
- try {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- // Reduce the idle time so that the originally issued refresh token is valid, but it will be considered invalid due to the client configuration
- clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, "500");
- client.update(clientRepresentation);
-
- oauth.client("test-app2", "password");
-
- // We are already logged in due to the token
- oauth.openLoginForm();
-
- String code2 = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse2 = oauth.doAccessTokenRequest(code2);
-
- assertThat(sessionId, Matchers.equalTo(tokenResponse2.getSessionState()));
-
- setTimeOffset(100);
-
- tokenResponse2 = oauth.doRefreshTokenRequest(tokenResponse2.getRefreshToken());
- assertEquals(200, tokenResponse2.getStatusCode());
- assertTrue("Invalid RefreshExpiresIn: " + tokenResponse2.getRefreshExpiresIn(), 0 < tokenResponse2.getRefreshExpiresIn() && tokenResponse2.getRefreshExpiresIn() <= 500);
-
- // Clear all entries from the cache to enforce re-loading the data from the database
- testingClient.server("test").run(session -> {
- InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
- if (connections != null) {
- Cache> sessionCache = connections.getCache(USER_SESSION_CACHE_NAME);
- Cache> clientSessionCache = connections.getCache(CLIENT_SESSION_CACHE_NAME);
- if (sessionCache != null) {
- sessionCache.clear();
- }
- if (clientSessionCache != null) {
- clientSessionCache.clear();
- }
- }
- });
-
- setTimeOffset(550);
- oauth.client("test-app", "password");
- events.poll();
-
- // The client session of the first client should have expired by now
- String refreshId = oauth.parseRefreshToken(tokenResponse.getRefreshToken()).getId();
- tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken());
-
- assertEquals(400, tokenResponse.getStatusCode());
- assertNull(tokenResponse.getAccessToken());
- assertNull(tokenResponse.getRefreshToken());
- events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
-
- } finally {
- clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, null);
- client.update(clientRepresentation);
-
- events.clear();
- resetTimeOffset();
- getTestingClient().testing().revertTestingInfinispanTimeService();
- }
- }
-
- @Test
- public void testCheckSsl() {
- try (Client client = AdminClientUtil.createResteasyClient()) {
- UriBuilder builder = UriBuilder.fromUri(AUTH_SERVER_ROOT);
- URI grantUri = OIDCLoginProtocolService.tokenUrl(builder).build("test");
- WebTarget grantTarget = client.target(grantUri);
- builder = UriBuilder.fromUri(AUTH_SERVER_ROOT);
- URI uri = OIDCLoginProtocolService.tokenUrl(builder).build("test");
- WebTarget refreshTarget = client.target(uri);
-
- String refreshToken;
- {
- Response response = executeGrantAccessTokenRequest(grantTarget);
- assertEquals(200, response.getStatus());
- org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
- refreshToken = tokenResponse.getRefreshToken();
- response.close();
- }
-
- {
- Response response = executeRefreshToken(refreshTarget, refreshToken);
- assertEquals(200, response.getStatus());
- org.keycloak.representations.AccessTokenResponse tokenResponse = response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
- refreshToken = tokenResponse.getRefreshToken();
- response.close();
- }
-
- if (!AUTH_SERVER_SSL_REQUIRED) { // test checkSsl
- RealmResource realmResource = adminClient.realm("test");
- {
- RealmManager.realm(realmResource).sslRequired(SslRequired.ALL.toString());
- }
-
- Response response = executeRefreshToken(refreshTarget, refreshToken);
- assertEquals(403, response.getStatus());
- response.close();
-
- {
- RealmManager.realm(realmResource).sslRequired(SslRequired.EXTERNAL.toString());
- }
- }
-
- Response response = executeRefreshToken(refreshTarget, refreshToken);
- assertEquals(200, response.getStatus());
- response.readEntity(org.keycloak.representations.AccessTokenResponse.class);
- response.close();
- } finally {
- events.clear();
- }
-
- }
-
- @Test
- public void refreshTokenUserDisabled() {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- String refreshTokenString = response.getRefreshToken();
- RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
-
- events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- try {
- UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(false);
- response = oauth.doRefreshTokenRequest(refreshTokenString);
- assertEquals(400, response.getStatusCode());
- assertEquals("invalid_grant", response.getError());
-
- events.expectRefresh(refreshToken.getId(), sessionId).user((String) null).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
- } finally {
- UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(true);
- }
- }
-
- @Test
- public void refreshTokenUserDeleted() {
- String userId = createUser("test", "temp-user@localhost", "password");
- oauth.doLogin("temp-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- String refreshTokenString = response.getRefreshToken();
- RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
-
- events.expectCodeToToken(codeId, sessionId).user(userId).assertEvent();
-
- adminClient.realm("test").users().delete(userId).close();
-
- response = oauth.doRefreshTokenRequest(refreshTokenString);
- assertEquals(400, response.getStatusCode());
- assertEquals("invalid_grant", response.getError());
-
- events.expectRefresh(refreshToken.getId(), sessionId).user((String) null).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
- }
-
- @Test
- public void refreshTokenServiceAccount() {
- AccessTokenResponse response = oauth.client("service-account-app", "secret").doClientCredentialsGrantAccessTokenRequest();
-
- assertNotNull(response.getRefreshToken());
-
- response = oauth.doRefreshTokenRequest(response.getRefreshToken());
-
- assertNotNull(response.getRefreshToken());
- }
-
- @Test
- public void testClientSessionMaxLifespan() {
- ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
- ClientRepresentation clientRepresentation = client.toRepresentation();
-
- RealmResource realm = adminClient.realm("test");
- RealmRepresentation rep = realm.toRepresentation();
- Integer originalSsoSessionMaxLifespan = rep.getSsoSessionMaxLifespan();
- int ssoSessionMaxLifespan = rep.getSsoSessionIdleTimeout() - 100;
- Integer originalClientSessionMaxLifespan = rep.getClientSessionMaxLifespan();
-
- try {
- rep.setSsoSessionMaxLifespan(ssoSessionMaxLifespan);
- realm.update(rep);
-
- oauth.doLogin("test-user@localhost", "password");
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- assertEquals(200, response.getStatusCode());
- assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan);
-
- rep.setClientSessionMaxLifespan(ssoSessionMaxLifespan - 100);
- realm.update(rep);
-
- String refreshToken = response.getRefreshToken();
- response = oauth.doRefreshTokenRequest(refreshToken);
- assertEquals(200, response.getStatusCode());
- assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan - 100);
-
- clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN,
- Integer.toString(ssoSessionMaxLifespan - 200));
- client.update(clientRepresentation);
-
- refreshToken = response.getRefreshToken();
- response = oauth.doRefreshTokenRequest(refreshToken);
- assertEquals(200, response.getStatusCode());
- assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan - 200);
- } finally {
- rep.setSsoSessionMaxLifespan(originalSsoSessionMaxLifespan);
- rep.setClientSessionMaxLifespan(originalClientSessionMaxLifespan);
- realm.update(rep);
- clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, null);
- client.update(clientRepresentation);
- }
- }
-
- @Test
- public void testClientSessionIdleTimeout() {
- ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
- ClientRepresentation clientRepresentation = client.toRepresentation();
-
- RealmResource realm = adminClient.realm("test");
- RealmRepresentation rep = realm.toRepresentation();
- int ssoSessionIdleTimeout = rep.getSsoSessionIdleTimeout();
- Integer originalClientSessionIdleTimeout = rep.getClientSessionIdleTimeout();
-
- try {
- oauth.doLogin("test-user@localhost", "password");
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- assertEquals(200, response.getStatusCode());
- assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout);
-
- rep.setClientSessionIdleTimeout(ssoSessionIdleTimeout - 100);
- realm.update(rep);
-
- String refreshToken = response.getRefreshToken();
- response = oauth.doRefreshTokenRequest(refreshToken);
- assertEquals(200, response.getStatusCode());
- assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout - 100);
-
- clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT,
- Integer.toString(ssoSessionIdleTimeout - 200));
- client.update(clientRepresentation);
-
- refreshToken = response.getRefreshToken();
- response = oauth.doRefreshTokenRequest(refreshToken);
- assertEquals(200, response.getStatusCode());
- assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout - 200);
- } finally {
- rep.setClientSessionIdleTimeout(originalClientSessionIdleTimeout);
- realm.update(rep);
- clientRepresentation.getAttributes().put(CLIENT_SESSION_IDLE_TIMEOUT, null);
- client.update(clientRepresentation);
- }
- }
-
- @Test // KEYCLOAK-17323
- public void testRefreshTokenWhenClientSessionTimeoutPassedButRealmDidNot() {
- //noinspection resource
- getCleanup()
- .addCleanup(new RealmAttributeUpdater(adminClient.realm("test"))
- .setSsoSessionIdleTimeout(2592000) // 30 Days
- .setSsoSessionMaxLifespan(86313600) // 999 Days
- .update()
- )
- .addCleanup(ClientAttributeUpdater.forClient(adminClient, "test", "test-app")
- .setAttribute(CLIENT_SESSION_IDLE_TIMEOUT, "60") // 1 minute
- .setAttribute(CLIENT_SESSION_MAX_LIFESPAN, "65") // 1 minute 5 seconds
- .update()
- );
-
- getTestingClient().testing().setTestingInfinispanTimeService();
- try {
- oauth.doLogin("test-user@localhost", "password");
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse response = oauth.doAccessTokenRequest(code);
- assertEquals(200, response.getStatusCode());
- assertExpiration(response.getExpiresIn(), 65);
-
- setTimeOffset(70);
-
- oauth.openLoginForm();
- code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse response2 = oauth.doAccessTokenRequest(code);
- assertExpiration(response2.getExpiresIn(), 65);
- } finally {
- getTestingClient().testing().revertTestingInfinispanTimeService();
- resetTimeOffset();
- }
- }
-
- @Test
- public void refreshTokenRequestNoRefreshToken() {
- ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
- ClientRepresentation clientRepresentation = client.toRepresentation();
-
- oauth.doLogin("test-user@localhost", "password");
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- String refreshTokenString = tokenResponse.getRefreshToken();
-
- clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "false");
- client.update(clientRepresentation);
- AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString);
-
- assertNotNull(response.getAccessToken());
- assertNull(response.getRefreshToken());
-
- clientRepresentation.getAttributes().put(OIDCConfigAttributes.USE_REFRESH_TOKEN, "true");
- client.update(clientRepresentation);
- }
-
- @Test
- public void tokenRefreshRequest_ClientRS384_RealmRS384() throws Exception {
- conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.RS384, Algorithm.RS384);
- }
-
- @Test
- public void tokenRefreshRequest_ClientRS512_RealmRS256() throws Exception {
- conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.RS512, Algorithm.RS256);
- }
-
- @Test
- public void tokenRefreshRequest_ClientES256_RealmRS256() throws Exception {
- conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.ES256, Algorithm.RS256);
- }
-
- @Test
- public void tokenRefreshRequest_ClientES384_RealmES384() throws Exception {
- conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.ES384, Algorithm.ES384);
- }
-
- @Test
- public void tokenRefreshRequest_ClientES512_RealmRS256() throws Exception {
- conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.ES512, Algorithm.RS256);
- }
-
- @Test
- public void tokenRefreshRequest_ClientPS256_RealmRS256() throws Exception {
- conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.PS256, Algorithm.RS256);
- }
-
- @Test
- public void tokenRefreshRequest_ClientPS384_RealmES384() throws Exception {
- conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.PS384, Algorithm.ES384);
- }
-
- @Test
- public void tokenRefreshRequest_ClientPS512_RealmPS256() throws Exception {
- conductTokenRefreshRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.PS512, Algorithm.PS256);
- }
-
- protected Response executeRefreshToken(WebTarget refreshTarget, String refreshToken) {
- String header = BasicAuthHelper.createHeader("test-app", "password");
- Form form = new Form();
- form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN);
- form.param("refresh_token", refreshToken);
- return refreshTarget.request()
- .header(HttpHeaders.AUTHORIZATION, header)
- .post(Entity.form(form));
- }
-
- protected Response executeGrantAccessTokenRequest(WebTarget grantTarget) {
- String header = BasicAuthHelper.createHeader("test-app", "password");
- Form form = new Form();
- form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD)
- .param("username", "test-user@localhost")
- .param("password", "password");
- return grantTarget.request()
- .header(HttpHeaders.AUTHORIZATION, header)
- .post(Entity.form(form));
- }
-
- private void conductTokenRefreshRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
- try {
- // Realm setting is used for ID Token signature algorithm
- TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, expectedIdTokenAlg);
- TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), expectedAccessAlg);
- refreshToken(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg);
- } finally {
- TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
- TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), Algorithm.RS256);
- }
- }
-
- private void refreshToken(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
- String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
- String code = oauth.parseLoginResponse().getCode();
-
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- JWSHeader header = new JWSInput(tokenResponse.getAccessToken()).getHeader();
- assertEquals(expectedAccessAlg, header.getAlgorithm().name());
- assertEquals("JWT", header.getType());
- assertNull(header.getContentType());
-
- header = new JWSInput(tokenResponse.getIdToken()).getHeader();
- assertEquals(expectedIdTokenAlg, header.getAlgorithm().name());
- assertEquals("JWT", header.getType());
- assertNull(header.getContentType());
-
- header = new JWSInput(tokenResponse.getRefreshToken()).getHeader();
- assertEquals(expectedRefreshAlg, header.getAlgorithm().name());
- assertEquals("JWT", header.getType());
- assertNull(header.getContentType());
-
- AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
- String refreshTokenString = tokenResponse.getRefreshToken();
- RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
-
- EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
-
- assertNotNull(refreshTokenString);
-
- assertEquals("Bearer", tokenResponse.getTokenType());
-
- assertEquals(sessionId, refreshToken.getSessionId());
-
- AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString);
- if (response.getError() != null || response.getErrorDescription() != null) {
- log.debugf("Refresh token error: %s, error description: %s", response.getError(), response.getErrorDescription());
- }
-
- AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
- RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(response.getRefreshToken());
-
- assertEquals(200, response.getStatusCode());
-
- assertEquals(sessionId, refreshedToken.getSessionId());
- assertEquals(sessionId, refreshedRefreshToken.getSessionId());
-
- Assert.assertNotEquals(token.getId(), refreshedToken.getId());
- Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId());
-
- assertEquals("Bearer", response.getTokenType());
-
- assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), refreshedToken.getSubject());
- // The following check is not valid anymore since file store does have the same ID, and is redundant due to the previous line
- // Assert.assertNotEquals("test-user@localhost", refreshedToken.getSubject());
-
- EventRepresentation refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent();
- Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
- Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
- }
-
- private String loginAndForceNewLoginPage() {
- oauth.doLogin("test-user@localhost", "password");
-
- EventRepresentation loginEvent = events.expectLogin().assertEvent();
-
- String sessionId = loginEvent.getSessionId();
-
- String code = oauth.parseLoginResponse().getCode();
- AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code);
-
- events.poll();
-
- // Assert refresh successful
- String refreshToken = tokenResponse.getRefreshToken();
- RefreshToken refreshTokenParsed1 = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
- processExpectedValidRefresh(sessionId, refreshTokenParsed1, refreshToken);
-
- // Open the tab with prompt=login. AuthenticationSession will be created with same ID like userSession
- String loginFormUri = oauth.loginForm()
- .param(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
- .build();
- driver.navigate().to(loginFormUri);
-
- loginPage.assertCurrent();
- Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername());
-
- return refreshToken;
- }
-}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/hok/HoKTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/hok/HoKTest.java
index 9ea98bafe68..8b921c8baf1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/hok/HoKTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/hok/HoKTest.java
@@ -46,7 +46,7 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.KeycloakTestingClient;
import org.keycloak.testsuite.drone.Different;
-import org.keycloak.testsuite.oauth.RefreshTokenTest;
+import org.keycloak.testsuite.oauth.OAuthProofKeyForCodeExchangeTest;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.testsuite.util.MutualTLSUtils;
@@ -358,7 +358,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
assertEquals("Bearer", tokenResponse.getTokenType());
assertThat(token.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(200L), lessThanOrEqualTo(350L)));
long actual = refreshToken.getExp() - getCurrentTime();
- assertThat(actual, allOf(greaterThanOrEqualTo(1799L - RefreshTokenTest.ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + RefreshTokenTest.ALLOWED_CLOCK_SKEW)));
+ assertThat(actual, allOf(greaterThanOrEqualTo(1799L - OAuthProofKeyForCodeExchangeTest.ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + OAuthProofKeyForCodeExchangeTest.ALLOWED_CLOCK_SKEW)));
assertEquals(sessionId, refreshToken.getSessionState());
setTimeOffset(2);
@@ -398,7 +398,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
assertEquals("Bearer", tokenResponse.getTokenType());
assertThat(token.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(200L), lessThanOrEqualTo(350L)));
long actual = refreshToken.getExp() - getCurrentTime();
- assertThat(actual, allOf(greaterThanOrEqualTo(1799L - RefreshTokenTest.ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + RefreshTokenTest.ALLOWED_CLOCK_SKEW)));
+ assertThat(actual, allOf(greaterThanOrEqualTo(1799L - OAuthProofKeyForCodeExchangeTest.ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800L + OAuthProofKeyForCodeExchangeTest.ALLOWED_CLOCK_SKEW)));
assertEquals(sessionId, refreshToken.getSessionState());
setTimeOffset(2);
@@ -437,7 +437,7 @@ public class HoKTest extends AbstractTestRealmKeycloakTest {
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
- assertThat(refreshedToken.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(250L - RefreshTokenTest.ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(300L + RefreshTokenTest.ALLOWED_CLOCK_SKEW)));
+ assertThat(refreshedToken.getExp() - getCurrentTime(), allOf(greaterThanOrEqualTo(250L - OAuthProofKeyForCodeExchangeTest.ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(300L + OAuthProofKeyForCodeExchangeTest.ALLOWED_CLOCK_SKEW)));
assertThat(refreshedToken.getExp() - token.getExp(), allOf(greaterThanOrEqualTo(1L), lessThanOrEqualTo(10L)));
assertThat(refreshedRefreshToken.getExp() - refreshToken.getExp(), allOf(greaterThanOrEqualTo(1L), lessThanOrEqualTo(10L)));
diff --git a/testsuite/integration-arquillian/tests/base/testsuites/clusterless-suite b/testsuite/integration-arquillian/tests/base/testsuites/clusterless-suite
index a6416cf07bf..bfc3ff39540 100644
--- a/testsuite/integration-arquillian/tests/base/testsuites/clusterless-suite
+++ b/testsuite/integration-arquillian/tests/base/testsuites/clusterless-suite
@@ -10,7 +10,6 @@ KcOidcUserSessionLimitsBrokerTest
KcSamlUserSessionLimitsBrokerTest
AbstractUserSessionLimitsBrokerTest
UserSessionLimitsTest
-RefreshTokenTest
OfflineTokenTest
AccessTokenTest
LogoutTest
diff --git a/testsuite/integration-arquillian/tests/base/testsuites/database-suite b/testsuite/integration-arquillian/tests/base/testsuites/database-suite
index 6e220916680..5db4afcdc70 100644
--- a/testsuite/integration-arquillian/tests/base/testsuites/database-suite
+++ b/testsuite/integration-arquillian/tests/base/testsuites/database-suite
@@ -8,7 +8,6 @@ KcOidcBrokerTest
LDAPUserLoginTest
LoginTest
PasswordPolicyTest
-RefreshTokenTest
RequiredActionUpdateProfileTest
SSOTest
SamlClientTest
diff --git a/testsuite/integration-arquillian/tests/base/testsuites/volatile-sessions-suite b/testsuite/integration-arquillian/tests/base/testsuites/volatile-sessions-suite
index 4a53f076292..becb8c922c2 100644
--- a/testsuite/integration-arquillian/tests/base/testsuites/volatile-sessions-suite
+++ b/testsuite/integration-arquillian/tests/base/testsuites/volatile-sessions-suite
@@ -11,7 +11,6 @@ KcOidcUserSessionLimitsBrokerTest
KcSamlUserSessionLimitsBrokerTest
AbstractUserSessionLimitsBrokerTest
UserSessionLimitsTest
-RefreshTokenTest
OfflineTokenTest
AccessTokenTest
LogoutTest