mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Refresh the login page when root auth session changes
Closes #32658 Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
parent
25e4995eb7
commit
612e2caae1
@ -19,6 +19,11 @@ public final class CookieType {
|
||||
.defaultMaxAge(CookieMaxAge.SESSION)
|
||||
.build();
|
||||
|
||||
public static final CookieType AUTH_SESSION_ID_HASH = CookieType.create("KC_AUTH_SESSION_HASH")
|
||||
.scope(CookieScope.INTERNAL_JS)
|
||||
.defaultMaxAge(60)
|
||||
.build();
|
||||
|
||||
public static final CookieType AUTH_SESSION_ID = CookieType.create("AUTH_SESSION_ID")
|
||||
.scope(CookieScope.FEDERATION)
|
||||
.defaultMaxAge(CookieMaxAge.SESSION)
|
||||
|
||||
@ -19,16 +19,24 @@
|
||||
|
||||
package org.keycloak.forms.login.freemarker.model;
|
||||
|
||||
import org.keycloak.crypto.JavaAlgorithm;
|
||||
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class AuthenticationSessionBean {
|
||||
|
||||
private final String authSessionId;
|
||||
private final String authSessionIdHash;
|
||||
private final String tabId;
|
||||
|
||||
public AuthenticationSessionBean(String authSessionId, String tabId) {
|
||||
this.authSessionId = authSessionId;
|
||||
this.authSessionIdHash = Base64.getEncoder().withoutPadding().encodeToString(HashUtils.hash(JavaAlgorithm.SHA256, authSessionId.getBytes(StandardCharsets.UTF_8)));
|
||||
this.tabId = tabId;
|
||||
}
|
||||
|
||||
@ -36,6 +44,10 @@ public class AuthenticationSessionBean {
|
||||
return authSessionId;
|
||||
}
|
||||
|
||||
public String getAuthSessionIdHash() {
|
||||
return authSessionIdHash;
|
||||
}
|
||||
|
||||
public String getTabId() {
|
||||
return tabId;
|
||||
}
|
||||
|
||||
@ -17,13 +17,17 @@
|
||||
|
||||
package org.keycloak.services.managers;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.cookie.CookieProvider;
|
||||
import org.keycloak.cookie.CookieType;
|
||||
import org.keycloak.crypto.JavaAlgorithm;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
@ -36,6 +40,7 @@ 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;
|
||||
|
||||
|
||||
/**
|
||||
@ -64,6 +69,7 @@ public class AuthenticationSessionManager {
|
||||
|
||||
if (browserCookie) {
|
||||
setAuthSessionCookie(rootAuthSession.getId());
|
||||
setAuthSessionIdHashCookie(rootAuthSession.getId());
|
||||
}
|
||||
|
||||
return rootAuthSession;
|
||||
@ -126,6 +132,17 @@ public class AuthenticationSessionManager {
|
||||
log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param authSessionId decoded authSessionId (without route info attached)
|
||||
*/
|
||||
public void setAuthSessionIdHashCookie(String authSessionId) {
|
||||
String authSessionIdHash = Base64.getEncoder().withoutPadding().encodeToString(HashUtils.hash(JavaAlgorithm.SHA256, authSessionId.getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
session.getProvider(CookieProvider.class).set(CookieType.AUTH_SESSION_ID_HASH, authSessionIdHash);
|
||||
|
||||
log.debugf("Set KC_AUTH_SESSION_HASH cookie with value %s", authSessionIdHash);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@ -70,6 +70,7 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
|
||||
Response response = testingInsecure.server("master").runWithResponse(session -> {
|
||||
CookieProvider cookies = session.getProvider(CookieProvider.class);
|
||||
cookies.set(CookieType.AUTH_SESSION_ID, "my-auth-session-id");
|
||||
cookies.set(CookieType.AUTH_SESSION_ID_HASH, "my-kc-auth-session");
|
||||
cookies.set(CookieType.AUTH_RESTART, "my-auth-restart");
|
||||
cookies.set(CookieType.AUTH_DETACHED, "my-auth-detached", 222);
|
||||
cookies.set(CookieType.IDENTITY, "my-identity", 333);
|
||||
@ -78,8 +79,9 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
|
||||
cookies.set(CookieType.SESSION, "my-session", 444);
|
||||
cookies.set(CookieType.WELCOME_CSRF, "my-welcome-csrf");
|
||||
});
|
||||
Assert.assertEquals(8, response.getCookies().size());
|
||||
Assert.assertEquals(9, response.getCookies().size());
|
||||
assertCookie(response, "AUTH_SESSION_ID", "my-auth-session-id", "/auth/realms/master/", -1, false, true, "Lax", true);
|
||||
assertCookie(response, "KC_AUTH_SESSION_HASH", "my-kc-auth-session", "/auth/realms/master/", 60, false, false, "Strict", true);
|
||||
assertCookie(response, "KC_RESTART", "my-auth-restart", "/auth/realms/master/", -1, false, true, "Lax", false);
|
||||
assertCookie(response, "KC_STATE_CHECKER", "my-auth-detached", "/auth/realms/master/", 222, false, true, "Strict", false);
|
||||
assertCookie(response, "KEYCLOAK_IDENTITY", "my-identity", "/auth/realms/master/", 333, false, true, "Lax", true);
|
||||
@ -183,6 +185,7 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
|
||||
response = testingClient.server("master").runWithResponse(session -> {
|
||||
CookieProvider cookies = session.getProvider(CookieProvider.class);
|
||||
cookies.set(CookieType.AUTH_SESSION_ID, "my-auth-session-id");
|
||||
cookies.set(CookieType.AUTH_SESSION_ID_HASH, "my-kc-auth-session");
|
||||
cookies.set(CookieType.AUTH_RESTART, "my-auth-restart");
|
||||
cookies.set(CookieType.AUTH_DETACHED, "my-auth-detached", 222);
|
||||
cookies.set(CookieType.IDENTITY, "my-identity", 333);
|
||||
@ -193,8 +196,9 @@ public class DefaultCookieProviderTest extends AbstractKeycloakTest {
|
||||
});
|
||||
}
|
||||
|
||||
Assert.assertEquals(8, response.getCookies().size());
|
||||
Assert.assertEquals(9, response.getCookies().size());
|
||||
assertCookie(response, "AUTH_SESSION_ID", "my-auth-session-id", "/auth/realms/master/", -1, false, true, "Lax", true);
|
||||
assertCookie(response, "KC_AUTH_SESSION_HASH", "my-kc-auth-session", "/auth/realms/master/", 60, false, false, "Strict", true);
|
||||
assertCookie(response, "KC_RESTART", "my-auth-restart", "/auth/realms/master/", -1, false, true, "Lax", false);
|
||||
assertCookie(response, "KC_STATE_CHECKER", "my-auth-detached", "/auth/realms/master/", 222, false, true, "Strict", false);
|
||||
assertCookie(response, "KEYCLOAK_IDENTITY", "my-identity", "/auth/realms/master/", 333, false, true, "Lax", true);
|
||||
|
||||
@ -246,22 +246,22 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||
oauth.openLoginForm();
|
||||
loginPage.assertCurrent();
|
||||
getLogger().info("URL in tab1: " + driver.getCurrentUrl());
|
||||
|
||||
// Open new tab 2
|
||||
tabUtil.newTab(oauth.getLoginFormUrl());
|
||||
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
|
||||
loginPage.assertCurrent();
|
||||
getLogger().info("URL in tab2: " + driver.getCurrentUrl());
|
||||
|
||||
// Wait until authentication session expires
|
||||
setTimeOffset(7200000);
|
||||
|
||||
//triggers the postponed function in authChecker.js to check if the auth session cookie has changed
|
||||
WaitUtils.pause(2000);
|
||||
|
||||
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
|
||||
loginPage.login("login-test", "password");
|
||||
loginPage.assertCurrent();
|
||||
Assert.assertEquals(loginPage.getError(), "Your login attempt timed out. Login will start from the beginning.");
|
||||
events.clear();
|
||||
|
||||
loginSuccessAndDoRequiredActions();
|
||||
|
||||
// Go back to tab1. Usually should be automatically authenticated here (previously it showed "You are already logged-in")
|
||||
@ -338,6 +338,9 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||
// Wait until authentication session expires
|
||||
setTimeOffset(7200000);
|
||||
|
||||
//triggers the postponed function in authChecker.js to check if the auth session cookie has changed
|
||||
WaitUtils.pause(2000);
|
||||
|
||||
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
|
||||
loginPage.login("login-test", "password");
|
||||
loginPage.assertCurrent();
|
||||
@ -374,6 +377,9 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||
// Wait until authentication session expires
|
||||
setTimeOffset(7200000);
|
||||
|
||||
//triggers the postponed function in authChecker.js to check if the auth session cookie has changed
|
||||
WaitUtils.pause(2000);
|
||||
|
||||
// Try to login in tab2. After fill login form, the login will be restarted (due KC_RESTART cookie). User can continue login
|
||||
loginPage.login("login-test", "password");
|
||||
loginPage.assertCurrent();
|
||||
@ -692,6 +698,44 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginPageRefresh() {
|
||||
|
||||
try (BrowserTabUtil tabUtil = BrowserTabUtil.getInstanceAndSetEnv(driver)) {
|
||||
assertThat(tabUtil.getCountOfTabs(), Matchers.is(1));
|
||||
oauth.openLoginForm();
|
||||
loginPage.assertCurrent();
|
||||
getLogger().info("URL in tab1: " + driver.getCurrentUrl());
|
||||
|
||||
//delete cookie to be recreated in tab 2
|
||||
driver.manage().deleteCookieNamed("AUTH_SESSION_ID");
|
||||
|
||||
// Open new tab 2
|
||||
tabUtil.newTab(oauth.getLoginFormUrl());
|
||||
assertThat(tabUtil.getCountOfTabs(), Matchers.equalTo(2));
|
||||
loginPage.assertCurrent();
|
||||
getLogger().info("URL in tab2: " + driver.getCurrentUrl());
|
||||
|
||||
tabUtil.switchToTab(0);
|
||||
loginPage.assertCurrent();
|
||||
|
||||
//wait for the refresh in the first tab
|
||||
WaitUtils.pause(2000);
|
||||
|
||||
if (driver instanceof HtmlUnitDriver) {
|
||||
// authChecker.js javascript does not work with HtmlUnitDriver. So need to "refresh" the current browser tab by running the last action in order to simulate "already_logged_in"
|
||||
// error and being redirected to client
|
||||
driver.navigate().refresh();
|
||||
}
|
||||
|
||||
loginSuccessAndDoRequiredActions();
|
||||
|
||||
tabUtil.switchToTab(1);
|
||||
|
||||
waitForAppPage(() -> loginPage.login("login-test", "password"));
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForAppPage(Runnable htmlUnitAction) {
|
||||
if (driver instanceof HtmlUnitDriver) {
|
||||
// authChecker.js javascript does not work with HtmlUnitDriver. So need to "refresh" the current browser tab by running the last action in order to simulate "already_logged_in"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const CHECK_INTERVAL_MILLISECS = 2000;
|
||||
const AUTH_SESSION_TIMEOUT_MILLISECS = 1000;
|
||||
const initialSession = getSession();
|
||||
|
||||
let timeout;
|
||||
@ -32,6 +33,19 @@ export function checkCookiesAndSetTimer(loginRestartUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
export function checkAuthSession(pageAuthSessionHash) {
|
||||
setTimeout(() => {
|
||||
const cookieAuthSessionHash = getKcAuthSessionHash();
|
||||
if (cookieAuthSessionHash !== pageAuthSessionHash) {
|
||||
location.reload();
|
||||
}
|
||||
}, AUTH_SESSION_TIMEOUT_MILLISECS);
|
||||
}
|
||||
|
||||
function getKcAuthSessionHash() {
|
||||
return getCookieByName("KC_AUTH_SESSION_HASH");
|
||||
}
|
||||
|
||||
function getSession() {
|
||||
return getCookieByName("KEYCLOAK_SESSION");
|
||||
}
|
||||
|
||||
@ -50,6 +50,15 @@
|
||||
"${url.ssoLoginInOtherTabsUrl?no_esc}"
|
||||
);
|
||||
</script>
|
||||
<#if authenticationSession??>
|
||||
<script type="module">
|
||||
import { checkAuthSession } from "${url.resourcesPath}/js/authChecker.js";
|
||||
|
||||
checkAuthSession(
|
||||
"${authenticationSession.authSessionIdHash}"
|
||||
);
|
||||
</script>
|
||||
</#if>
|
||||
</head>
|
||||
|
||||
<body class="${properties.kcBodyClass!}">
|
||||
|
||||
@ -92,6 +92,15 @@
|
||||
"${url.ssoLoginInOtherTabsUrl?no_esc}"
|
||||
);
|
||||
</script>
|
||||
<#if authenticationSession??>
|
||||
<script type="module">
|
||||
import { checkAuthSession } from "${url.resourcesPath}/js/authChecker.js";
|
||||
|
||||
checkAuthSession(
|
||||
"${authenticationSession.authSessionIdHash}"
|
||||
);
|
||||
</script>
|
||||
</#if>
|
||||
<script>
|
||||
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1404468
|
||||
const isFirefox;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user