diff --git a/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc index 290d57ec60f..fc54047708a 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_1_0.adoc @@ -57,11 +57,14 @@ Users should use the TCP based `jdbc-ping` stack as a direct replacement. When developing extensions for {project_name}, developers can now specify dependencies between provider factories classes by implementing the method `dependsOn()` in the `ProviderFactory` interface. See the Javadoc for a detailed description. -= Updated format of KEYCLOAK_SESSION cookie += Updated format of KEYCLOAK_SESSION cookie and AUTH_SESSION_ID cookie -The format of `KEYCLOAK_SESSION` cookie was slightly updated to not contain any private data in plain text. Until now, the format of the cookie was `realmName/userId/userSessionId`. Now the cookie -contains user session ID, which is hashed by SHA-256 and URL encoded. This can affect you just in case when implementing your own providers and relying on the format of internal {project_name} -cookies. +The format of `KEYCLOAK_SESSION` cookie was slightly updated to not contain any private data in plain text. Until now, the format of the cookie was `realmName/userId/userSessionId`. Now the cookie contains user session ID, which is hashed by SHA-256 and URL encoded. + + +The format of `AUTH_SESSION_ID` cookie was updated to include a signature of the auth session id to ensure its integrity through signature verification. The new format is `base64(auth_session_id.auth_session_id_signature)`. With this update, the old format will no longer be accepted, meaning that old auth sessions will no longer be valid. This change has no impact on user sessions. + +These changes can affect you just in case when implementing your own providers and relying on the format of internal Keycloak cookies. = Removal of robots.txt file @@ -88,4 +91,4 @@ expect the database schema being updated to add a new column `DETAILS_JSON` to t The key providers that allow to import externally generated keys (`rsa` and `java-keystore` factories) now check the validity of the associated certificate if present. Therefore a key with a certificate that is expired cannot be imported in {project_name} anymore. If the certificate expires at runtime, the key is converted into a passive key (enabled but not active). A passive key is not used for new tokens, but it is still valid for validating previous issued tokens. -The default `generated` key providers generate a certificate valid for 10 years (the types that have or can have an associated certificate). Because of the long validity and the recommendation to rotate keys frequently, the generated providers do not perform this check. \ No newline at end of file +The default `generated` key providers generate a certificate valid for 10 years (the types that have or can have an associated certificate). Because of the long validity and the recommendation to rotate keys frequently, the generated providers do not perform this check. diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 447ef5ec237..a75c3d4956e 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -151,6 +151,23 @@ public final class KeycloakModelUtils { return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } + /** + * Check if a string is a valid UUID. + * @param uuid The UUID string to verify + * @return true if the string is a valid uuid + */ + public static boolean isValidUUID(String uuid) { + if (uuid == null) { + return false; + } + try { + UUID.fromString(uuid); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + public static PublicKey getPublicKey(String publicKeyPem) { if (publicKeyPem != null) { try { diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java index 75dc446da31..440d2894dd2 100644 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java @@ -22,17 +22,22 @@ import java.util.Base64; import java.util.Objects; import org.jboss.logging.Logger; +import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Time; import org.keycloak.cookie.CookieProvider; import org.keycloak.cookie.CookieType; import org.keycloak.crypto.JavaAlgorithm; +import org.keycloak.crypto.SignatureProvider; +import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.jose.jws.crypto.HashUtils; import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.SessionExpiration; import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.sessions.AuthenticationSessionModel; @@ -40,7 +45,6 @@ import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.StickySessionEncoderProvider; import static org.keycloak.services.managers.AuthenticationManager.authenticateIdentityCookie; -import static org.keycloak.services.managers.AuthenticationManager.setKcActionStatus; /** @@ -75,7 +79,6 @@ public class AuthenticationSessionManager { return rootAuthSession; } - public RootAuthenticationSessionModel getCurrentRootAuthenticationSession(RealmModel realm) { String oldEncodedId = getAuthSessionCookies(realm); if (oldEncodedId == null) { @@ -119,17 +122,17 @@ public class AuthenticationSessionManager { } } - /** * @param authSessionId decoded authSessionId (without route info attached) */ public void setAuthSessionCookie(String authSessionId) { StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class); - String encodedAuthSessionId = encoder.encodeSessionId(authSessionId); + String signedAuthSessionId = signAndEncodeToBase64AuthSessionId(authSessionId); + String encodedWithRoute = encoder.encodeSessionId(signedAuthSessionId); - session.getProvider(CookieProvider.class).set(CookieType.AUTH_SESSION_ID, encodedAuthSessionId); + session.getProvider(CookieProvider.class).set(CookieType.AUTH_SESSION_ID, encodedWithRoute); - log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId); + log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedWithRoute); } /** @@ -146,7 +149,7 @@ public class AuthenticationSessionManager { /** * - * @param encodedAuthSessionId encoded ID with attached route in cluster environment (EG. "5e161e00-d426-4ea6-98e9-52eb9844e2d7.node1" ) + * @param encodedAuthSessionId encoded ID with attached route in cluster environment (EG. "NWUxNjFlMDAtZDQyNi00ZWE2LTk4ZTktNTJlYjk4NDRlMmQ3L.node1" ) * @return object with decoded and actually encoded authSessionId */ AuthSessionId decodeAuthSessionId(String encodedAuthSessionId) { @@ -155,10 +158,13 @@ public class AuthenticationSessionManager { String decodedAuthSessionId = encoder.decodeSessionId(encodedAuthSessionId); String reencoded = encoder.encodeSessionId(decodedAuthSessionId); + if (!KeycloakModelUtils.isValidUUID(decodedAuthSessionId)) { + decodedAuthSessionId = decodeBase64AndValidateSignature(decodedAuthSessionId, false); + } + return new AuthSessionId(decodedAuthSessionId, reencoded); } - void reencodeAuthSessionCookie(String oldEncodedAuthSessionId, AuthSessionId newAuthSessionId, RealmModel realm) { if (!oldEncodedAuthSessionId.equals(newAuthSessionId.getEncodedId())) { log.debugf("Route changed. Will update authentication session cookie. Old: '%s', New: '%s'", oldEncodedAuthSessionId, @@ -167,10 +173,58 @@ public class AuthenticationSessionManager { } } + public String decodeBase64AndValidateSignature(String encodedBase64AuthSessionId, boolean validate) { + try { + String decodedAuthSessionId = new String(Base64Url.decode(encodedBase64AuthSessionId), StandardCharsets.UTF_8); + if (decodedAuthSessionId.lastIndexOf(".") != -1) { + String authSessionId = decodedAuthSessionId.substring(0, decodedAuthSessionId.indexOf(".")); + String signature = decodedAuthSessionId.substring(decodedAuthSessionId.indexOf(".") + 1); + return validate ? validateAuthSessionIdSignature(authSessionId, signature) : authSessionId; + } + } catch (Exception e) { + log.errorf("Error decoding auth session id with value: %s", encodedBase64AuthSessionId, e); + } + return null; + } + + private String validateAuthSessionIdSignature(String authSessionId, String signature) { + //check if the signature has already been verified for the same request + if(signature.equals(session.getAttribute(authSessionId))) { + return authSessionId; + } + + SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, Constants.INTERNAL_SIGNATURE_ALGORITHM); + SignatureSignerContext signer = signatureProvider.signer(); + try { + boolean valid = signatureProvider.verifier(signer.getKid()).verify(authSessionId.getBytes(StandardCharsets.UTF_8), Base64Url.decode(signature)); + if (!valid) { + return null; + } + //Save the signature to avoid re-verification for the same request + session.setAttribute(authSessionId, signature); + return authSessionId; + } catch (Exception e) { + log.errorf("Signature validation failed for auth session id: %s", authSessionId, e); + } + return null; + } + + private String signAndEncodeToBase64AuthSessionId(String authSessionId) { + SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, Constants.INTERNAL_SIGNATURE_ALGORITHM); + SignatureSignerContext signer = signatureProvider.signer(); + StringBuilder buffer = new StringBuilder(); + byte[] signature = signer.sign(authSessionId.getBytes(StandardCharsets.UTF_8)); + buffer.append(authSessionId); + if (signature != null) { + buffer.append('.'); + buffer.append(Base64Url.encode(signature)); + } + return Base64Url.encode(buffer.toString().getBytes(StandardCharsets.UTF_8)); + } /** * @param realm - * @return the value of the AUTH_SESSION_ID cookie. It is assumed that values could be encoded with route added (EG. "5e161e00-d426-4ea6-98e9-52eb9844e2d7.node1" ) + * @return the value of the AUTH_SESSION_ID cookie. It is assumed that values could be encoded with signature and with route added (EG. "NWUxNjFlMDAtZDQyNi00ZWE2LTk4ZTktNTJlYjk4NDRlMmQ3L.node1" ) */ String getAuthSessionCookies(RealmModel realm) { String oldEncodedId = session.getProvider(CookieProvider.class).get(CookieType.AUTH_SESSION_ID); @@ -178,17 +232,22 @@ public class AuthenticationSessionManager { return null; } - StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class); + StickySessionEncoderProvider routeEncoder = session.getProvider(StickySessionEncoderProvider.class); // in case the id is encoded with a route when running in a cluster - String decodedId = encoder.decodeSessionId(oldEncodedId); + String decodedAuthSessionId = routeEncoder.decodeSessionId(oldEncodedId); + + decodedAuthSessionId = decodeBase64AndValidateSignature(decodedAuthSessionId, true); + if(decodedAuthSessionId == null) { + return null; + } + // we can't blindly trust the cookie and assume it is valid and referencing a valid root auth session // but make sure the root authentication session actually exists // without this check there is a risk of resolving user sessions from invalid root authentication sessions as they share the same id - RootAuthenticationSessionModel rootAuthenticationSession = session.authenticationSessions().getRootAuthenticationSession(realm, decodedId); + RootAuthenticationSessionModel rootAuthenticationSession = session.authenticationSessions().getRootAuthenticationSession(realm, decodedAuthSessionId); return rootAuthenticationSession != null ? oldEncodedId : null; } - public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) { RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession(); @@ -265,6 +324,11 @@ public class AuthenticationSessionManager { return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client, tabId); } + public AuthenticationSessionModel getAuthenticationSessionByEncodedIdAndClient(RealmModel realm, String encodedAuthSesionId, ClientModel client, String tabId) { + String decodedAuthSessionId = decodeBase64AndValidateSignature(encodedAuthSesionId, true); + return getAuthenticationSessionByIdAndClient(realm, decodedAuthSessionId, client, tabId); + } + public UserSessionModel getUserSessionFromAuthenticationCookie(RealmModel realm) { String oldEncodedId = getAuthSessionCookies(realm); diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java index 6078b5b8180..72581b34c22 100644 --- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java @@ -162,7 +162,7 @@ public class SessionCodeChecks { AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session); AuthenticationSessionModel authSession = null; if (authSessionId != null) - authSession = authSessionManager.getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId); + authSession = authSessionManager.getAuthenticationSessionByEncodedIdAndClient(realm, authSessionId, client, tabId); AuthenticationSessionModel authSessionCookie = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId); if (authSession != null && authSessionCookie != null && !authSession.getParentSession().getId().equals(authSessionCookie.getParentSession().getId())) { diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 87a7a36f34a..1c00ccdd073 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -80,6 +80,7 @@ import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.scheduled.ClearExpiredUserSessions; import org.keycloak.sessions.RootAuthenticationSessionModel; @@ -1155,7 +1156,8 @@ public class TestingResourceProvider implements RealmResourceProvider { @NoCache public Integer getAuthenticationSessionTabsCount(@QueryParam("realm") String realmName, @QueryParam("authSessionId") String authSessionId) { RealmModel realm = getRealmByName(realmName); - RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId); + String decodedAuthSessionId = new AuthenticationSessionManager(session).decodeBase64AndValidateSignature(authSessionId, false); + RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, decodedAuthSessionId); if (rootAuthSession == null) { return 0; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java index 32b16192f1e..47cc234a8c4 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java @@ -37,6 +37,7 @@ import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.updaters.RealmAttributeUpdater; @@ -110,7 +111,8 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct testingClient.server().run(session -> { // ensure that our logic to detect the authentication session works as expected RealmModel realm = session.realms().getRealm(TEST_REALM_NAME); - assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); + String decodedAuthSessionId = new AuthenticationSessionManager(session).decodeBase64AndValidateSignature(authSessionId, false); + assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, decodedAuthSessionId)); }); changePasswordPage.changePassword("new-password", "new-password"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java index 86272b74ca3..91b7a89f98b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java @@ -149,6 +149,7 @@ import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer; import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.resources.RealmsResource; import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.testsuite.adapter.page.*; @@ -1891,7 +1892,8 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest { final String authSessionId = sessionCookie.getValue(); testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName(DEMO); - RootAuthenticationSessionModel root = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId); + String decodedAuthSessionId = new AuthenticationSessionManager(session).decodeBase64AndValidateSignature(authSessionId, false); + RootAuthenticationSessionModel root = session.authenticationSessions().getRootAuthenticationSession(realm, decodedAuthSessionId); session.authenticationSessions().removeRootAuthenticationSession(realm, root); }); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionClusterTest.java index 94b01970dd8..06a896968e7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionClusterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionClusterTest.java @@ -27,6 +27,7 @@ import org.junit.Test; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanUtil; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.sessions.StickySessionEncoderProvider; import org.keycloak.sessions.StickySessionEncoderProviderFactory; import org.keycloak.testsuite.pages.AppPage; @@ -95,8 +96,8 @@ public class AuthenticationSessionClusterTest extends AbstractClusterTest { driver.navigate().to(testAppLoginNode1URL); String authSessionCookie = AuthenticationSessionFailoverClusterTest.getAuthSessionCookieValue(driver); - assertThat(authSessionCookie.length(), Matchers.greaterThan(36)); - String route = authSessionCookie.substring(37); + Assert.assertNotEquals( -1, authSessionCookie.indexOf(".")); + String route = authSessionCookie.substring(authSessionCookie.indexOf(".") + 1); visitedRoutes.add(route); // Drop all cookies before continue @@ -126,7 +127,7 @@ public class AuthenticationSessionClusterTest extends AbstractClusterTest { driver.navigate().to(testAppLoginNode1URL); String authSessionCookie = AuthenticationSessionFailoverClusterTest.getAuthSessionCookieValue(driver); - Assert.assertEquals(36, authSessionCookie.length()); + Assert.assertEquals(authSessionCookie.indexOf("."), -1); // Drop all cookies before continue driver.manage().deleteAllCookies(); @@ -134,7 +135,8 @@ public class AuthenticationSessionClusterTest extends AbstractClusterTest { // Check that route owner is always node1 getTestingClientFor(backendNode(0)).server().run(session -> { Cache authSessionCache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME); - String keyOwner = InfinispanUtil.getTopologyInfo(session).getRouteName(authSessionCache, authSessionCookie); + String decodedAuthSessionId = new AuthenticationSessionManager(session).decodeBase64AndValidateSignature(authSessionCookie, false); + String keyOwner = InfinispanUtil.getTopologyInfo(session).getRouteName(authSessionCache, decodedAuthSessionId); Assert.assertTrue(keyOwner.startsWith("node1")); }); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 5ed26a90c73..5cbf844a361 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.forms; import java.io.Closeable; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -33,6 +34,7 @@ import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.common.Profile; +import org.keycloak.common.util.Base64Url; import org.keycloak.cookie.CookieType; import org.keycloak.crypto.Algorithm; import org.keycloak.events.Details; @@ -45,6 +47,7 @@ import org.keycloak.models.ClientScopeModel; import org.keycloak.models.Constants; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; @@ -1031,4 +1034,21 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { } } + + @Test + public void testAuthSessionIdCookieFormat(){ + oauth.openLoginForm(); + String encodedBase64AuthSessionId = driver.manage().getCookieNamed(CookieType.AUTH_SESSION_ID.getName()).getValue(); + String decodedAuthSessionId = new String(Base64Url.decode(encodedBase64AuthSessionId), StandardCharsets.UTF_8); + Assert.assertTrue(decodedAuthSessionId.contains(".")); + String authSessionId = decodedAuthSessionId.substring(0, decodedAuthSessionId.indexOf(".")); + String signature = decodedAuthSessionId.substring(decodedAuthSessionId.indexOf(".") + 1); + Assert.assertNotNull(authSessionId); + Assert.assertTrue(KeycloakModelUtils.isValidUUID(authSessionId)); + Assert.assertNotNull(signature); + + testingClient.server().run(session-> { + Assert.assertNotNull(session.authenticationSessions().getRootAuthenticationSession(session.getContext().getRealm(), authSessionId)); + }); + } }