Skip to content

Commit

Permalink
Changing locale on logout confirmation did not work
Browse files Browse the repository at this point in the history
  • Loading branch information
mposolda committed May 30, 2022
1 parent 3da4eec commit 232a38c
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 11 deletions.
Expand Up @@ -494,6 +494,9 @@ protected void createCommonAttributes(Theme theme, Locale locale, Properties mes
case REGISTER:
b = UriBuilder.fromUri(Urls.realmRegisterPage(baseUri, realm.getName()));
break;
case LOGOUT_CONFIRM:
b = UriBuilder.fromUri(Urls.logoutConfirm(baseUri, realm.getName()));
break;
default:
b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
break;
Expand Down
Expand Up @@ -38,6 +38,7 @@
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.SystemClientUtil;
import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
Expand Down Expand Up @@ -65,6 +66,7 @@
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.LogoutSessionCodeChecks;
import org.keycloak.services.resources.SessionCodeChecks;
import org.keycloak.services.util.LocaleUtil;
import org.keycloak.services.util.MtlsHoKTokenUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
Expand Down Expand Up @@ -258,9 +260,12 @@ public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class)
.setAuthenticationSession(logoutSession);

UserSessionModel userSession = null;

// Check if we have session in the browser. If yes and it is different session than referenced by id_token_hint, the confirmation should be displayed
AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, false);
if (authResult != null) {
userSession = authResult.getSession();
if (idToken != null && idToken.getSessionState() != null && !idToken.getSessionState().equals(authResult.getSession().getId())) {
forcedConfirmation = true;
}
Expand All @@ -272,6 +277,17 @@ public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String
}
}

if (userSession == null && idToken != null && idToken.getSessionState() != null) {
userSession = session.sessions().getUserSession(realm, idToken.getSessionState());
}

// Try to figure user because of localization
if (userSession != null) {
UserModel user = userSession.getUser();
logoutSession.setAuthenticatedUser(user);
loginForm.setUser(user);
}

// Logout confirmation screen will be displayed to the user in this case
if (confirmationNeeded || forcedConfirmation) {
return displayLogoutConfirmationScreen(loginForm, logoutSession);
Expand All @@ -297,6 +313,7 @@ private Response displayLogoutConfirmationScreen(LoginFormsProvider loginForm, A
* @return response
*/
@POST
@NoCache
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response logout() {
MultivaluedMap<String, String> form = request.getDecodedFormParameters();
Expand All @@ -315,6 +332,7 @@ public Response logout() {

@Path("/logout-confirm")
@POST
@NoCache
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response logoutConfirmAction() {
MultivaluedMap<String, String> formData = request.getDecodedFormParameters();
Expand All @@ -327,7 +345,7 @@ public Response logoutConfirmAction() {

SessionCodeChecks checks = new LogoutSessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, code, clientId, tabId);
checks.initialVerify();
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.LOGGING_OUT.name(), ClientSessionCode.ActionType.USER) || !formData.containsKey("confirmLogout")) {
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.LOGGING_OUT.name(), ClientSessionCode.ActionType.USER) || !checks.isActionRequest() || !formData.containsKey("confirmLogout")) {
AuthenticationSessionModel logoutSession = checks.getAuthenticationSession();
logger.debugf("Failed verification during logout. logoutSessionId=%s, clientId=%s, tabId=%s",
logoutSession != null ? logoutSession.getParentSession().getId() : "unknown", clientId, tabId);
Expand All @@ -347,6 +365,48 @@ public Response logoutConfirmAction() {
}


// Typically shown when user changes localization on the logout confirmation screen
@Path("/logout-confirm")
@NoCache
@GET
public Response logoutConfirmGet() {
event.event(EventType.LOGOUT);

String clientId = session.getContext().getUri().getQueryParameters().getFirst(Constants.CLIENT_ID);
String tabId = session.getContext().getUri().getQueryParameters().getFirst(Constants.TAB_ID);

logger.tracef("Changing localization by user during logout. clientId=%s, tabId=%s, kc_locale: %s", clientId, tabId, session.getContext().getUri().getQueryParameters().getFirst(LocaleSelectorProvider.KC_LOCALE_PARAM));

SessionCodeChecks checks = new LogoutSessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, null, clientId, tabId);
AuthenticationSessionModel logoutSession = checks.initialVerifyAuthSession();
if (logoutSession == null) {
logger.debugf("Failed verification when changing locale logout. clientId=%s, tabId=%s", clientId, tabId);

LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class);
if (clientId == null || clientId.equals(SystemClientUtil.getSystemClient(realm).getClientId())) {
// Cleanup system client URL to avoid links to account management
loginForm.setAttribute(Constants.SKIP_LINK, true);
}

AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, false);
if (authResult != null) {
return ErrorPage.error(session, logoutSession, Response.Status.BAD_REQUEST, Messages.FAILED_LOGOUT);
} else {
// Probably changing locale on logout screen after logout was already performed. If there is no session in the browser, we can just display that logout was already finished
return loginForm.setSuccess(Messages.SUCCESS_LOGOUT).createInfoPage();
}
}

LocaleUtil.processLocaleParam(session, realm, logoutSession);

LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class)
.setAuthenticationSession(logoutSession)
.setUser(logoutSession.getAuthenticatedUser());

return displayLogoutConfirmationScreen(loginForm, logoutSession);
}


// Method triggered after user eventually confirmed that he wants to logout and all other checks were done
private Response doBrowserLogout(AuthenticationSessionModel logoutSession) {
UserSessionModel userSession = null;
Expand Down
Expand Up @@ -374,10 +374,10 @@ public static AuthenticationSessionModel createOrJoinLogoutSession(KeycloakSessi
} else {
logoutAuthSession = rootLogoutSession.createAuthenticationSession(client);
logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
session.getContext().setClient(client);
logger.tracef("Creating logout session for client '%s'. Authentication session id: %s", client.getClientId(), rootLogoutSession.getId());
}
session.getContext().setAuthenticationSession(logoutAuthSession);
session.getContext().setClient(client);

return logoutAuthSession;
}
Expand Down
Expand Up @@ -83,6 +83,7 @@
import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.services.util.BrowserHistoryHelper;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.LocaleUtil;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
Expand Down Expand Up @@ -277,15 +278,7 @@ public Response authenticate(@QueryParam(AUTH_SESSION_ID) String authSessionId,
}

protected void processLocaleParam(AuthenticationSessionModel authSession) {
if (authSession != null && realm.isInternationalizationEnabled()) {
String locale = session.getContext().getUri().getQueryParameters().getFirst(LocaleSelectorProvider.KC_LOCALE_PARAM);
if (locale != null) {
authSession.setAuthNote(LocaleSelectorProvider.USER_REQUEST_LOCALE, locale);

LocaleUpdaterProvider localeUpdater = session.getProvider(LocaleUpdaterProvider.class);
localeUpdater.updateLocaleCookie(locale);
}
}
LocaleUtil.processLocaleParam(session, realm, authSession);
}

protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
Expand Down
43 changes: 43 additions & 0 deletions services/src/main/java/org/keycloak/services/util/LocaleUtil.java
@@ -0,0 +1,43 @@
/*
* Copyright 2022 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.services.util;

import org.keycloak.locale.LocaleSelectorProvider;
import org.keycloak.locale.LocaleUpdaterProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.sessions.AuthenticationSessionModel;

/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LocaleUtil {

public static void processLocaleParam(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession) {
if (authSession != null && realm.isInternationalizationEnabled()) {
String locale = session.getContext().getUri().getQueryParameters().getFirst(LocaleSelectorProvider.KC_LOCALE_PARAM);
if (locale != null) {
authSession.setAuthNote(LocaleSelectorProvider.USER_REQUEST_LOCALE, locale);

LocaleUpdaterProvider localeUpdater = session.getProvider(LocaleUpdaterProvider.class);
localeUpdater.updateLocaleCookie(locale);
}
}
}
}
Expand Up @@ -35,6 +35,7 @@
import org.keycloak.events.Errors;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.IDToken;
Expand All @@ -53,12 +54,14 @@

import java.io.Closeable;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.is;
Expand All @@ -76,6 +79,7 @@
import org.keycloak.testsuite.pages.PageUtils;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.updaters.UserAttributeUpdater;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.OAuthClient;
Expand Down Expand Up @@ -770,6 +774,129 @@ public void logoutWithPostRequest() throws IOException {
}


@Test
public void testLocalizationPreferenceDuringLogout() throws IOException {
try (RealmAttributeUpdater realmUpdater = new RealmAttributeUpdater(testRealm()).addSupportedLocale("cs").update()) {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();

// Set localization to the user account to "cs". Ensure that it is shown
try (UserAttributeUpdater userUpdater = UserAttributeUpdater.forUserByUsername(testRealm(), "test-user@localhost").setAttribute(UserModel.LOCALE, "cs").update()) {
driver.navigate().to(oauth.getLogoutUrl().build());
Assert.assertEquals("Odhlašování", PageUtils.getPageTitle(driver)); // Logging out
Assert.assertEquals("Čeština", logoutConfirmPage.getLanguageDropdownText());

// Set localization together with ui_locales param. User localization should have preference
driver.navigate().to(oauth.getLogoutUrl().uiLocales("de").build());
Assert.assertEquals("Odhlašování", PageUtils.getPageTitle(driver)); // Logging out
Assert.assertEquals("Čeština", logoutConfirmPage.getLanguageDropdownText());
}

UserAttributeUpdater.forUserByUsername(testRealm(), "test-user@localhost").removeAttribute(UserModel.LOCALE).update();

// Removed localization from user account. Now localization set by ui_locales parameter should be used
driver.navigate().to(oauth.getLogoutUrl().uiLocales("de").build());
Assert.assertEquals("Abmelden", PageUtils.getPageTitle(driver)); // Logging out
Assert.assertEquals("Deutsch", logoutConfirmPage.getLanguageDropdownText());
logoutConfirmPage.confirmLogout();
WaitUtils.waitForPageToLoad();
events.expectLogout(tokenResponse.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();

// Remove ui_locales from logout request. Default locale should be set
tokenResponse = loginUser();
driver.navigate().to(oauth.getLogoutUrl().build());
Assert.assertEquals("Logging out", PageUtils.getPageTitle(driver));
Assert.assertEquals("English", logoutConfirmPage.getLanguageDropdownText());
logoutConfirmPage.confirmLogout();
WaitUtils.waitForPageToLoad();
events.expectLogout(tokenResponse.getSessionState()).removeDetail(Details.REDIRECT_URI).assertEvent();
}
}


@Test
public void testLocalizationDuringLogout() throws IOException {
try (RealmAttributeUpdater realmUpdater = new RealmAttributeUpdater(testRealm()).addSupportedLocale("cs").update()) {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();

// Display the logout page. Then change the localization to Czech, then back to english and then and logout
driver.navigate().to(oauth.getLogoutUrl().build());

logoutConfirmPage.assertCurrent();
logoutConfirmPage.openLanguage("Čeština");

Assert.assertEquals("Odhlašování", PageUtils.getPageTitle(driver)); // Logging out
Assert.assertEquals("Čeština", logoutConfirmPage.getLanguageDropdownText());

logoutConfirmPage.openLanguage("English");

Assert.assertEquals("Logging out", PageUtils.getPageTitle(driver));
Assert.assertEquals("English", logoutConfirmPage.getLanguageDropdownText());

// Logout
logoutConfirmPage.confirmLogout();
infoPage.assertCurrent();
Assert.assertEquals("You are logged out", infoPage.getInfo());
try {
logoutConfirmPage.clickBackToApplicationLink();
fail();
}
catch (NoSuchElementException ex) {
// expected
}

// Display logout with ui_locales parameter set to "de"
tokenResponse = loginUser();
driver.navigate().to(oauth.getLogoutUrl()
.clientId("test-app")
.uiLocales("de")
.build());

Assert.assertEquals("Abmelden", PageUtils.getPageTitle(driver)); // Logging out
Assert.assertEquals("Deutsch", logoutConfirmPage.getLanguageDropdownText());

// Change locale. It should have preference over the "de" set by ui_locales
logoutConfirmPage.openLanguage("Čeština");
Assert.assertEquals("Odhlašování", PageUtils.getPageTitle(driver)); // Logging out
Assert.assertEquals("Čeština", logoutConfirmPage.getLanguageDropdownText());

// Logout
logoutConfirmPage.confirmLogout();

infoPage.assertCurrent();
Assert.assertEquals("Odhlášení bylo úspěšné", infoPage.getInfo()); // Logout success message
infoPage.clickBackToApplicationLinkCs();
WaitUtils.waitForPageToLoad();
Assert.assertThat(driver.getCurrentUrl(), endsWith("/app/auth"));
}
}


@Test
public void testIncorrectChangingParameters() throws IOException {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();

// Display the logout page. Then change the localization to Czech and logout
driver.navigate().to(oauth.getLogoutUrl().uiLocales("de").build());

Assert.assertEquals("Abmelden", PageUtils.getPageTitle(driver)); // Logging out
logoutConfirmPage.openLanguage("English");

// Try to manually change value of parameter tab_id to some incorrect value. Error should be shown in this case
String currentUrl = driver.getCurrentUrl();
String changedUrl = UriBuilder.fromUri(currentUrl)
.replaceQueryParam(Constants.TAB_ID, "invalid")
.build().toString();

driver.navigate().to(changedUrl);
WaitUtils.waitForPageToLoad();

errorPage.assertCurrent();
Assert.assertEquals("Logout failed", errorPage.getError());

events.expectLogoutError(Errors.SESSION_EXPIRED).assertEvent();
}


@Test
public void testFrontChannelLogoutWithPostLogoutRedirectUri() throws Exception {
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
Expand Down

0 comments on commit 232a38c

Please sign in to comment.