KEYCLOAK-11417 Internal server error on front channel logout with expired session
This commit is contained in:
parent
f45e187c35
commit
3ddedc49f5
3 changed files with 67 additions and 12 deletions
|
@ -54,7 +54,11 @@ import org.keycloak.services.resources.Cors;
|
||||||
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
@ -65,6 +69,9 @@ import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.keycloak.models.UserSessionModel.State.LOGGED_OUT;
|
||||||
|
import static org.keycloak.models.UserSessionModel.State.LOGGING_OUT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
|
@ -152,11 +159,15 @@ public class LogoutEndpoint {
|
||||||
if (idToken != null && idToken.getSessionState().equals(AuthenticationManager.getSessionIdFromSessionCookie(session))) {
|
if (idToken != null && idToken.getSessionState().equals(AuthenticationManager.getSessionIdFromSessionCookie(session))) {
|
||||||
return initiateBrowserLogout(userSession, redirect, state, initiatingIdp);
|
return initiateBrowserLogout(userSession, redirect, state, initiatingIdp);
|
||||||
}
|
}
|
||||||
|
// check if the user session is not logging out or already logged out
|
||||||
|
// this might happen when a backChannelLogout is already initiated from AuthenticationManager.authenticateIdentityCookie
|
||||||
|
if (userSession.getState() != LOGGING_OUT && userSession.getState() != LOGGED_OUT) {
|
||||||
// non browser logout
|
// non browser logout
|
||||||
event.event(EventType.LOGOUT);
|
event.event(EventType.LOGOUT);
|
||||||
AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, true);
|
AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, true);
|
||||||
event.user(userSession.getUser()).session(userSession).success();
|
event.user(userSession.getUser()).session(userSession).success();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (redirect != null) {
|
if (redirect != null) {
|
||||||
UriBuilder uriBuilder = UriBuilder.fromUri(redirect);
|
UriBuilder uriBuilder = UriBuilder.fromUri(redirect);
|
||||||
|
|
|
@ -25,13 +25,10 @@ import org.keycloak.common.util.ServerCookie;
|
||||||
|
|
||||||
import javax.ws.rs.core.Cookie;
|
import javax.ws.rs.core.Cookie;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
|
import static org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
|
||||||
|
|
||||||
|
@ -100,7 +97,7 @@ public class CookieHelper {
|
||||||
Set<String> ret = getInternalCookieValue(name);
|
Set<String> ret = getInternalCookieValue(name);
|
||||||
if (ret.size() == 0) {
|
if (ret.size() == 0) {
|
||||||
String legacy = name + LEGACY_COOKIE;
|
String legacy = name + LEGACY_COOKIE;
|
||||||
logger.debugv("Couldn't find any cookies with name '{0}', trying '{1}'", name, legacy);
|
logger.debugv("Could not find any cookies with name '{0}', trying '{1}'", name, legacy);
|
||||||
ret = getInternalCookieValue(legacy);
|
ret = getInternalCookieValue(legacy);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -116,7 +113,7 @@ public class CookieHelper {
|
||||||
// get cookies from the cookie field
|
// get cookies from the cookie field
|
||||||
Cookie cookie = headers.getCookies().get(name);
|
Cookie cookie = headers.getCookies().get(name);
|
||||||
if (cookie != null) {
|
if (cookie != null) {
|
||||||
logger.debugv("{0} cookie found in the cookie's field", name);
|
logger.debugv("{0} cookie found in the cookie field", name);
|
||||||
cookiesVal.add(cookie.getValue());
|
cookiesVal.add(cookie.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +131,7 @@ public class CookieHelper {
|
||||||
|
|
||||||
for (Cookie cookie : CookieParser.parseCookies(header)) {
|
for (Cookie cookie : CookieParser.parseCookies(header)) {
|
||||||
if (name.equals(cookie.getName())) {
|
if (name.equals(cookie.getName())) {
|
||||||
logger.debugv("{0} cookie found in the request's header", name);
|
logger.debugv("{0} cookie found in the request header", name);
|
||||||
values.add(cookie.getValue());
|
values.add(cookie.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,7 +146,7 @@ public class CookieHelper {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
String legacy = name + LEGACY_COOKIE;
|
String legacy = name + LEGACY_COOKIE;
|
||||||
logger.debugv("Couldn't find cookie {0}, trying {1}", name, legacy);
|
logger.debugv("Could not find cookie {0}, trying {1}", name, legacy);
|
||||||
return cookies.get(legacy);
|
return cookies.get(legacy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,10 @@
|
||||||
package org.keycloak.testsuite.forms;
|
package org.keycloak.testsuite.forms;
|
||||||
|
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
@ -31,10 +33,12 @@ import org.keycloak.common.util.Retry;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
@ -44,6 +48,10 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
|
||||||
|
|
||||||
import org.keycloak.testsuite.auth.page.account.AccountManagement;
|
import org.keycloak.testsuite.auth.page.account.AccountManagement;
|
||||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||||
|
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||||
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -63,10 +71,18 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
|
||||||
@Page
|
@Page
|
||||||
protected AccountManagement accountManagementPage;
|
protected AccountManagement accountManagementPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
private ErrorPage errorPage;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void clientConfiguration() {
|
||||||
|
ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void logoutRedirect() {
|
public void logoutRedirect() {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
|
@ -118,6 +134,37 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertNotEquals(sessionId, sessionId2);
|
assertNotEquals(sessionId, sessionId2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void logoutWithExpiredSession() throws Exception {
|
||||||
|
try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm("test"))
|
||||||
|
.updateWith(r -> r.setSsoSessionMaxLifespan(2))
|
||||||
|
.update()) {
|
||||||
|
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
oauth.clientSessionState("client-session");
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
String idTokenString = tokenResponse.getIdToken();
|
||||||
|
|
||||||
|
// wait for a timeout
|
||||||
|
// setTimeOffset doesn't work because session cookie is not invalidated thus the logout flow would continue with browser logout
|
||||||
|
TimeUnit.SECONDS.sleep(3);
|
||||||
|
|
||||||
|
String logoutUrl = oauth.getLogoutUrl().redirectUri(oauth.APP_AUTH_ROOT).idTokenHint(idTokenString).build();
|
||||||
|
driver.navigate().to(logoutUrl);
|
||||||
|
|
||||||
|
// should not throw an internal server error
|
||||||
|
appPage.assertCurrent();
|
||||||
|
|
||||||
|
// check if the back channel logout succeeded
|
||||||
|
driver.navigate().to(oauth.getLoginFormUrl());
|
||||||
|
WaitUtils.waitForPageToLoad();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void logoutMultipleSessions() throws IOException {
|
public void logoutMultipleSessions() throws IOException {
|
||||||
// Login session 1
|
// Login session 1
|
||||||
|
|
Loading…
Reference in a new issue