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