Fix updating locale on info/error page after authenticationSession was already removed
Closes #13922
This commit is contained in:
parent
e3e123b577
commit
ccbddb2258
22 changed files with 474 additions and 60 deletions
|
@ -139,6 +139,14 @@ public interface LoginFormsProvider extends Provider {
|
|||
|
||||
LoginFormsProvider setInfo(String message, Object ... parameters);
|
||||
|
||||
LoginFormsProvider setMessage(MessageType type, String message, Object... parameters);
|
||||
|
||||
/**
|
||||
* Used when authenticationSession was already removed for this browser session and hence we don't have any
|
||||
* authenticationSession or user data. Would just repeat previous info/error page after language is changed
|
||||
*/
|
||||
LoginFormsProvider setDetachedAuthSession();
|
||||
|
||||
LoginFormsProvider setUser(UserModel user);
|
||||
|
||||
LoginFormsProvider setResponseHeader(String headerName, String headerValue);
|
||||
|
|
30
server-spi-private/src/main/java/org/keycloak/forms/login/MessageType.java
Executable file
30
server-spi-private/src/main/java/org/keycloak/forms/login/MessageType.java
Executable file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2023 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.forms.login;
|
||||
|
||||
/**
|
||||
* Enum with types of messages.
|
||||
*
|
||||
* @author Vlastimil Elias (velias at redhat dot com)
|
||||
*/
|
||||
public enum MessageType {
|
||||
|
||||
SUCCESS, WARNING, INFO, ERROR
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright 2023 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.forms.login.freemarker;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class DetachedInfoStateChecker {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DetachedInfoStateChecker.class);
|
||||
|
||||
private static final String STATE_CHECKER_COOKIE_NAME = "KC_STATE_CHECKER";
|
||||
public static final String STATE_CHECKER_PARAM = "kc_state_checker";
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final RealmModel realm;
|
||||
|
||||
public DetachedInfoStateChecker(KeycloakSession session, RealmModel realm) {
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
public DetachedInfoStateCookie generateAndSetCookie(String messageKey, String messageType, Integer status, String clientId, Object[] messageParameters) {
|
||||
UriInfo uriInfo = session.getContext().getHttpRequest().getUri();
|
||||
String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
|
||||
boolean secureOnly = realm.getSslRequired().isRequired(session.getContext().getConnection());
|
||||
|
||||
String currentStateCheckerInUrl = uriInfo.getQueryParameters().getFirst(STATE_CHECKER_PARAM);
|
||||
String newStateChecker = KeycloakModelUtils.generateId();
|
||||
int cookieMaxAge = realm.getAccessCodeLifespanUserAction();
|
||||
|
||||
DetachedInfoStateCookie cookie = new DetachedInfoStateCookie();
|
||||
cookie.setMessageKey(messageKey);
|
||||
cookie.setMessageType(messageType);
|
||||
cookie.setStatus(status);
|
||||
cookie.setClientUuid(clientId);
|
||||
cookie.setCurrentUrlState(currentStateCheckerInUrl);
|
||||
cookie.setRenderedUrlState(newStateChecker);
|
||||
if (messageParameters != null) {
|
||||
List<String> params = Stream.of(messageParameters)
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.toList());
|
||||
cookie.setMessageParameters(params);
|
||||
}
|
||||
|
||||
String encoded = session.tokens().encode(cookie);
|
||||
logger.tracef("Generating new %s cookie. Cookie: %s, Cookie lifespan: %d", STATE_CHECKER_COOKIE_NAME, cookie, cookieMaxAge);
|
||||
|
||||
CookieHelper.addCookie(STATE_CHECKER_COOKIE_NAME, encoded, path, null, null, cookieMaxAge, secureOnly, true, session);
|
||||
return cookie;
|
||||
}
|
||||
|
||||
public DetachedInfoStateCookie verifyStateCheckerParameter(String stateCheckerParam) throws VerificationException {
|
||||
Set<String> cookieVal = CookieHelper.getCookieValue(session, STATE_CHECKER_COOKIE_NAME);
|
||||
if (cookieVal == null || cookieVal.isEmpty()) {
|
||||
throw new VerificationException("State checker cookie is empty");
|
||||
}
|
||||
if (stateCheckerParam == null || stateCheckerParam.isEmpty()) {
|
||||
throw new VerificationException("State checker parameter is empty");
|
||||
}
|
||||
|
||||
String cookieEncoded = cookieVal.iterator().next();
|
||||
DetachedInfoStateCookie cookie = session.tokens().decode(cookieEncoded, DetachedInfoStateCookie.class);
|
||||
if (cookie == null) {
|
||||
throw new VerificationException("Failed to verify DetachedInfoStateCookie");
|
||||
}
|
||||
|
||||
// May want to compare with the currentUrlState (when refreshing detached info/error page) or with renderedUrlState (when user changes locale on the info/error page through the combobox).
|
||||
// As the currentUrlState is in the browser URL when renderedUrlState is in the link inside the user's combobox
|
||||
if (stateCheckerParam.equals(cookie.getCurrentUrlState()) || stateCheckerParam.equals(cookie.getRenderedUrlState())) {
|
||||
return cookie;
|
||||
} else {
|
||||
throw new VerificationException(String.format("Failed to verify state. StateCheckerParameter: %s, cookie current state checker: %s, Cookie rendered state checker: %s",
|
||||
stateCheckerParam, cookie.getCurrentUrlState(), cookie.getRenderedUrlState()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright 2023 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.forms.login.freemarker;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.keycloak.Token;
|
||||
import org.keycloak.TokenCategory;
|
||||
|
||||
/**
|
||||
* Cookie encapsulating data to be displayed on the info/error page. We need this data due the fact that authenticationSession may not exists.
|
||||
* This is needed so the info/error page can be restored after user changed language.
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class DetachedInfoStateCookie implements Token {
|
||||
|
||||
@JsonProperty("mky")
|
||||
private String messageKey;
|
||||
|
||||
@JsonProperty("mty")
|
||||
private String messageType;
|
||||
|
||||
@JsonProperty("mpar")
|
||||
private List<String> messageParameters;
|
||||
|
||||
@JsonProperty("stat")
|
||||
private Integer status;
|
||||
|
||||
@JsonProperty("clid")
|
||||
private String clientUuid;
|
||||
|
||||
@JsonProperty("st1")
|
||||
private String currentUrlState;
|
||||
|
||||
@JsonProperty("st2")
|
||||
private String renderedUrlState;
|
||||
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
|
||||
public void setMessageKey(String messageKey) {
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
|
||||
public String getMessageType() {
|
||||
return messageType;
|
||||
}
|
||||
|
||||
public void setMessageType(String messageType) {
|
||||
this.messageType = messageType;
|
||||
}
|
||||
|
||||
public List<String> getMessageParameters() {
|
||||
return messageParameters;
|
||||
}
|
||||
|
||||
public void setMessageParameters(List<String> messageParameters) {
|
||||
this.messageParameters = messageParameters;
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getClientUuid() {
|
||||
return clientUuid;
|
||||
}
|
||||
|
||||
public void setClientUuid(String clientUuid) {
|
||||
this.clientUuid = clientUuid;
|
||||
}
|
||||
|
||||
public String getCurrentUrlState() {
|
||||
return currentUrlState;
|
||||
}
|
||||
|
||||
public void setCurrentUrlState(String currentUrlState) {
|
||||
this.currentUrlState = currentUrlState;
|
||||
}
|
||||
|
||||
public String getRenderedUrlState() {
|
||||
return renderedUrlState;
|
||||
}
|
||||
|
||||
public void setRenderedUrlState(String renderedUrlState) {
|
||||
this.renderedUrlState = renderedUrlState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TokenCategory getCategory() {
|
||||
return TokenCategory.INTERNAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("DetachedInfoStateCookie [ ")
|
||||
.append("messageKey=" + messageKey)
|
||||
.append(", messageType=" + messageType)
|
||||
.append(", status=" + status)
|
||||
.append(", clientUuid=" + clientUuid)
|
||||
.append(", messageParameters=" + messageParameters)
|
||||
.append(", currentUrlState=" + currentUrlState)
|
||||
.append(", renderedUrlState=" + renderedUrlState)
|
||||
.append(" ]")
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -86,7 +86,7 @@ import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
|
|||
import org.keycloak.theme.beans.LocaleBean;
|
||||
import org.keycloak.theme.beans.MessageBean;
|
||||
import org.keycloak.theme.beans.MessageFormatterMethod;
|
||||
import org.keycloak.theme.beans.MessageType;
|
||||
import org.keycloak.forms.login.MessageType;
|
||||
import org.keycloak.theme.beans.MessagesPerFieldBean;
|
||||
import org.keycloak.theme.freemarker.FreeMarkerProvider;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
@ -112,6 +112,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
protected MessageType messageType = MessageType.ERROR;
|
||||
|
||||
protected MultivaluedMap<String, String> formData;
|
||||
protected boolean detachedAuthSession = false;
|
||||
|
||||
protected KeycloakSession session;
|
||||
/** authenticationSession can be null for some renderings, mainly error pages */
|
||||
|
@ -490,6 +491,22 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
case LOGOUT_CONFIRM:
|
||||
b = UriBuilder.fromUri(Urls.logoutConfirm(baseUri, realm.getName()));
|
||||
break;
|
||||
case INFO:
|
||||
case ERROR:
|
||||
if (isDetachedAuthenticationSession()) {
|
||||
FormMessage formMessage = getFirstMessage();
|
||||
if (formMessage == null) {
|
||||
throw new IllegalStateException("Not able to create info/error page with detached authentication session as no info/error message available");
|
||||
}
|
||||
|
||||
DetachedInfoStateCookie cookie = new DetachedInfoStateChecker(session, realm).generateAndSetCookie(
|
||||
formMessage.getMessage(), messageType.toString(), status == null ? null : status.getStatusCode(),
|
||||
client == null ? null : client.getId(), formMessage.getParameters());
|
||||
|
||||
b = UriBuilder.fromUri(Urls.loginActionsDetachedInfo(baseUri, realm.getName()))
|
||||
.queryParam(DetachedInfoStateChecker.STATE_CHECKER_PARAM, cookie.getRenderedUrlState());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
|
||||
break;
|
||||
|
@ -703,17 +720,24 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
return createResponse(LoginFormsPages.LOGOUT_CONFIRM);
|
||||
}
|
||||
|
||||
protected void setMessage(MessageType type, String message, Object... parameters) {
|
||||
@Override
|
||||
public LoginFormsProvider setMessage(MessageType type, String message, Object... parameters) {
|
||||
messageType = type;
|
||||
messages = new ArrayList<>();
|
||||
messages.add(new FormMessage(null, message, parameters));
|
||||
return this;
|
||||
}
|
||||
|
||||
private FormMessage getFirstMessage() {
|
||||
if (messages != null && !messages.isEmpty()) {
|
||||
return messages.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String getFirstMessageUnformatted() {
|
||||
if (messages != null && !messages.isEmpty()) {
|
||||
return messages.get(0).getMessage();
|
||||
}
|
||||
return null;
|
||||
FormMessage formMessage = getFirstMessage();
|
||||
return formMessage == null ? null : formMessage.getMessage();
|
||||
}
|
||||
|
||||
protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) {
|
||||
|
@ -783,6 +807,16 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginFormsProvider setDetachedAuthSession() {
|
||||
detachedAuthSession = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean isDetachedAuthenticationSession() {
|
||||
return detachedAuthSession || authenticationSession == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginFormsProvider setAuthenticationSession(AuthenticationSessionModel authenticationSession) {
|
||||
this.authenticationSession = authenticationSession;
|
||||
|
|
|
@ -75,14 +75,6 @@ public class UrlBean {
|
|||
return Urls.realmRegisterPage(baseURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getLoginUpdatePasswordUrl() {
|
||||
return Urls.loginActionUpdatePassword(baseURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getLoginUpdateTotpUrl() {
|
||||
return Urls.loginActionUpdateTotp(baseURI, realm).toString();
|
||||
}
|
||||
|
||||
public String getLoginUpdateProfileUrl() {
|
||||
return Urls.loginActionUpdateProfile(baseURI, realm).toString();
|
||||
}
|
||||
|
|
|
@ -92,11 +92,7 @@ public class DefaultLocaleSelectorProvider implements LocaleSelectorProvider {
|
|||
}
|
||||
|
||||
private Locale getUserSelectedLocale(RealmModel realm, AuthenticationSessionModel session) {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String locale = session.getAuthNote(USER_REQUEST_LOCALE);
|
||||
String locale = session == null ? this.session.getAttribute(USER_REQUEST_LOCALE, String.class) : session.getAuthNote(USER_REQUEST_LOCALE);
|
||||
if (locale == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.keycloak.models.Constants;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.SystemClientUtil;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
|
@ -51,7 +50,9 @@ public class LogoutUtil {
|
|||
if (usedSystemClient) {
|
||||
loginForm.setAttribute(Constants.SKIP_LINK, true);
|
||||
}
|
||||
return loginForm.createInfoPage();
|
||||
return loginForm
|
||||
.setDetachedAuthSession()
|
||||
.createInfoPage();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -107,12 +107,8 @@ public class Urls {
|
|||
return realmLogout(baseUri).path(LogoutEndpoint.class, "logoutConfirmAction").build(realmName);
|
||||
}
|
||||
|
||||
public static URI loginActionUpdatePassword(URI baseUri, String realmName) {
|
||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "updatePassword").build(realmName);
|
||||
}
|
||||
|
||||
public static URI loginActionUpdateTotp(URI baseUri, String realmName) {
|
||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "updateTotp").build(realmName);
|
||||
public static URI loginActionsDetachedInfo(URI baseUri, String realmName) {
|
||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "detachedInfo").build(realmName);
|
||||
}
|
||||
|
||||
public static UriBuilder requiredActionBase(URI baseUri) {
|
||||
|
|
|
@ -16,7 +16,7 @@ import org.keycloak.theme.Theme;
|
|||
import org.keycloak.theme.beans.LocaleBean;
|
||||
import org.keycloak.theme.beans.MessageBean;
|
||||
import org.keycloak.theme.beans.MessageFormatterMethod;
|
||||
import org.keycloak.theme.beans.MessageType;
|
||||
import org.keycloak.forms.login.MessageType;
|
||||
import org.keycloak.theme.freemarker.FreeMarkerProvider;
|
||||
import org.keycloak.utils.MediaType;
|
||||
import org.keycloak.utils.MediaTypeMatcher;
|
||||
|
|
|
@ -1072,6 +1072,7 @@ public class AuthenticationManager {
|
|||
infoPage.setAttribute(Constants.SKIP_LINK, true);
|
||||
}
|
||||
Response response = infoPage
|
||||
.setDetachedAuthSession()
|
||||
.createInfoPage();
|
||||
|
||||
new AuthenticationSessionManager(session).removeAuthenticationSession(authSession.getRealm(), authSession, true);
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
package org.keycloak.services.managers;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.util.ServerCookie.SameSiteAttributeValue;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -222,6 +222,9 @@ public class AuthenticationSessionManager {
|
|||
if (expireRestartCookie) {
|
||||
UriInfo uriInfo = session.getContext().getUri();
|
||||
RestartLoginCookie.expireRestartCookie(realm, uriInfo, session);
|
||||
|
||||
// With browser session, this makes sure that info/error pages will be rendered correctly when locale is changed on them
|
||||
session.getProvider(LoginFormsProvider.class).setDetachedAuthSession();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,10 @@ package org.keycloak.services.resources;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.ResponseSessionTask;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.forms.login.MessageType;
|
||||
import org.keycloak.forms.login.freemarker.DetachedInfoStateChecker;
|
||||
import org.keycloak.forms.login.freemarker.DetachedInfoStateCookie;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
|
@ -122,6 +126,8 @@ public class LoginActionsService {
|
|||
|
||||
public static final String RESTART_PATH = "restart";
|
||||
|
||||
public static final String DETACHED_INFO_PATH = "detached-info";
|
||||
|
||||
public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage";
|
||||
|
||||
public static final String SESSION_CODE = "session_code";
|
||||
|
@ -243,6 +249,50 @@ public class LoginActionsService {
|
|||
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* protocol independent "detached info" page. Shown when locale is changed by user on info/error page
|
||||
* after authenticationSession was already removed.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path(DETACHED_INFO_PATH)
|
||||
@GET
|
||||
public Response detachedInfo(@QueryParam(DetachedInfoStateChecker.STATE_CHECKER_PARAM) String stateCheckerParam) {
|
||||
DetachedInfoStateCookie cookie;
|
||||
try {
|
||||
cookie = new DetachedInfoStateChecker(session, realm).verifyStateCheckerParameter(stateCheckerParam);
|
||||
logger.tracef("Detached info endpoint invoked and cookie successfully verified. StateCheckerParam=%s, StateCookie=%s", stateCheckerParam, cookie);
|
||||
} catch (VerificationException ve) {
|
||||
logger.warn(ve.getMessage());
|
||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.EXPIRED_ACTION_TOKEN_NO_SESSION);
|
||||
}
|
||||
|
||||
processLocaleParam(null);
|
||||
|
||||
boolean skipLink = true;
|
||||
if (cookie.getClientUuid() != null) {
|
||||
ClientModel client = session.clients().getClientById(realm, cookie.getClientUuid());
|
||||
if (client != null) {
|
||||
session.getContext().setClient(client);
|
||||
skipLink = client.equals(SystemClientUtil.getSystemClient(realm));
|
||||
}
|
||||
}
|
||||
|
||||
MessageType type = Enum.valueOf(MessageType.class, cookie.getMessageType());
|
||||
Response.Status statusObj = cookie.getStatus() == null ? Response.Status.BAD_REQUEST : Response.Status.fromStatusCode(cookie.getStatus());
|
||||
Object[] paramsAsObject = cookie.getMessageParameters() == null ? null : cookie.getMessageParameters().toArray();
|
||||
|
||||
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class)
|
||||
.setDetachedAuthSession()
|
||||
.setMessage(type, cookie.getMessageKey(), paramsAsObject);
|
||||
|
||||
if (skipLink) {
|
||||
loginForm.setAttribute(Constants.SKIP_LINK, true);
|
||||
}
|
||||
|
||||
return type == MessageType.ERROR ? loginForm.createErrorPage(statusObj) : loginForm.createInfoPage();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* protocol independent login page entry point
|
||||
|
|
|
@ -222,6 +222,7 @@ public class SessionCodeChecks {
|
|||
ClientModel client = authSession.getClient();
|
||||
if (client == null) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
session.getProvider(LoginFormsProvider.class).setDetachedAuthSession();
|
||||
response = ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.UNKNOWN_LOGIN_REQUESTER);
|
||||
clientCode.removeExpiredClientSession();
|
||||
return false;
|
||||
|
@ -232,6 +233,7 @@ public class SessionCodeChecks {
|
|||
|
||||
if (checkClientDisabled(client)) {
|
||||
event.error(Errors.CLIENT_DISABLED);
|
||||
session.getProvider(LoginFormsProvider.class).setDetachedAuthSession();
|
||||
response = ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
|
||||
clientCode.removeExpiredClientSession();
|
||||
return false;
|
||||
|
|
|
@ -43,10 +43,15 @@ public class LocaleUtil {
|
|||
}
|
||||
|
||||
public static void processLocaleParam(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession) {
|
||||
if (authSession != null && realm.isInternationalizationEnabled()) {
|
||||
if (realm.isInternationalizationEnabled()) {
|
||||
String locale = session.getContext().getUri().getQueryParameters().getFirst(LocaleSelectorProvider.KC_LOCALE_PARAM);
|
||||
if (locale != null) {
|
||||
if (authSession != null) {
|
||||
authSession.setAuthNote(LocaleSelectorProvider.USER_REQUEST_LOCALE, locale);
|
||||
} else {
|
||||
// Might be on info/error page when we don't have authenticationSession
|
||||
session.setAttribute(LocaleSelectorProvider.USER_REQUEST_LOCALE, locale);
|
||||
}
|
||||
|
||||
LocaleUpdaterProvider localeUpdater = session.getProvider(LocaleUpdaterProvider.class);
|
||||
localeUpdater.updateLocaleCookie(locale);
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.theme.beans;
|
||||
|
||||
import org.keycloak.forms.login.MessageType;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
|
|
|
@ -1,28 +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.theme.beans;
|
||||
|
||||
/**
|
||||
* Enum with types of messages.
|
||||
*
|
||||
* @author Vlastimil Elias (velias at redhat dot com)
|
||||
*/
|
||||
public enum MessageType {
|
||||
|
||||
SUCCESS, WARNING, INFO, ERROR
|
||||
|
||||
}
|
|
@ -19,6 +19,8 @@ package org.keycloak.theme.beans;
|
|||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.forms.login.MessageType;
|
||||
|
||||
/**
|
||||
* Bean used to hold form messages per field. Stored under <code>messagesPerField</code> key in Freemarker context.
|
||||
*
|
||||
|
|
|
@ -26,7 +26,7 @@ import org.openqa.selenium.support.FindBy;
|
|||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ErrorPage extends AbstractPage {
|
||||
public class ErrorPage extends LanguageComboboxAwarePage {
|
||||
|
||||
@ArquillianResource
|
||||
protected OAuthClient oauth;
|
||||
|
|
|
@ -146,6 +146,7 @@ public class EmailTest extends AbstractI18NTest {
|
|||
}
|
||||
|
||||
//KEYCLOAK-7478
|
||||
// Issue 13922
|
||||
@Test
|
||||
public void changeLocaleOnInfoPage() throws InterruptedException, IOException {
|
||||
UserResource testUser = ApiUtil.findUserByUsernameId(testRealm(), "login-test");
|
||||
|
@ -178,5 +179,15 @@ public class EmailTest extends AbstractI18NTest {
|
|||
|
||||
Assert.assertTrue("Expected to be on InfoPage, but it was on " + DroneUtils.getCurrentDriver().getTitle(), infoPage.isCurrent());
|
||||
assertThat(infoPage.getInfo(), containsString("Your account has been updated."));
|
||||
|
||||
// Change language again when on final info page with the message about updated account (authSession removed already at this point)
|
||||
infoPage.openLanguage("Deutsch");
|
||||
assertEquals("Deutsch", infoPage.getLanguageDropdownText());
|
||||
assertThat(infoPage.getInfo(), containsString("Ihr Benutzerkonto wurde aktualisiert."));
|
||||
|
||||
infoPage.openLanguage("English");
|
||||
assertEquals("English", infoPage.getLanguageDropdownText());
|
||||
assertThat(infoPage.getInfo(), containsString("Your account has been updated."));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
package org.keycloak.testsuite.i18n;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
|
@ -29,12 +31,15 @@ import org.junit.Test;
|
|||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.adapters.HttpClientBuilder;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.forms.login.freemarker.DetachedInfoStateChecker;
|
||||
import org.keycloak.locale.LocaleSelectorProvider;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LanguageComboboxAwarePage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
|
||||
|
@ -62,6 +67,9 @@ public class LoginPageTest extends AbstractI18NTest {
|
|||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Page
|
||||
protected LoginPasswordUpdatePage changePasswordPage;
|
||||
|
||||
|
@ -255,6 +263,55 @@ public class LoginPageTest extends AbstractI18NTest {
|
|||
Assert.assertNull(localeCookie);
|
||||
}
|
||||
|
||||
|
||||
// Test for user updating locale on the error page (when authenticationSession is not available)
|
||||
@Test
|
||||
public void languageUserUpdatesOnErrorPage() {
|
||||
// Login page with invalid redirect_uri
|
||||
oauth.redirectUri("http://invalid");
|
||||
loginPage.open();
|
||||
|
||||
errorPage.assertCurrent();
|
||||
Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
|
||||
|
||||
// Change language should be OK
|
||||
errorPage.openLanguage("Deutsch");
|
||||
assertEquals("Deutsch", errorPage.getLanguageDropdownText());
|
||||
Assert.assertEquals("Ungültiger Parameter: redirect_uri", errorPage.getError());
|
||||
|
||||
// Refresh browser button should keep german language
|
||||
driver.navigate().refresh();
|
||||
assertEquals("Deutsch", errorPage.getLanguageDropdownText());
|
||||
Assert.assertEquals("Ungültiger Parameter: redirect_uri", errorPage.getError());
|
||||
|
||||
// Changing to english should work
|
||||
errorPage.openLanguage("English");
|
||||
assertEquals("English", errorPage.getLanguageDropdownText());
|
||||
Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void languageUserUpdatesOnErrorPageStateCheckerTest() throws URISyntaxException {
|
||||
// Login page with invalid redirect_uri
|
||||
oauth.redirectUri("http://invalid");
|
||||
loginPage.open();
|
||||
|
||||
errorPage.assertCurrent();
|
||||
Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError());
|
||||
|
||||
errorPage.openLanguage("Deutsch");
|
||||
Assert.assertEquals("Ungültiger Parameter: redirect_uri", errorPage.getError());
|
||||
|
||||
// Add incorrect state checker parameter. Error page should be shown about expired action. Language won't be changed
|
||||
String currentUrl = driver.getCurrentUrl();
|
||||
String newUrl = KeycloakUriBuilder.fromUri(new URI(currentUrl))
|
||||
.replaceQueryParam(LocaleSelectorProvider.KC_LOCALE_PARAM, "en")
|
||||
.replaceQueryParam(DetachedInfoStateChecker.STATE_CHECKER_PARAM, "invalid").buildAsString();
|
||||
driver.navigate().to(newUrl);
|
||||
|
||||
Assert.assertEquals("Die Aktion ist nicht mehr gültig.", errorPage.getError()); // Action expired.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realmLocalizationMessagesAreApplied() {
|
||||
String realmLocalizationMessageKey = "loginAccountTitle";
|
||||
|
|
|
@ -901,6 +901,19 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
infoPage.assertCurrent();
|
||||
Assert.assertEquals("Odhlášení bylo úspěšné", infoPage.getInfo()); // Logout success message
|
||||
|
||||
// Change locale on the info page (AuthenticationSession does not exists on server at this point)
|
||||
infoPage.openLanguage("English");
|
||||
Assert.assertEquals("You are logged out", infoPage.getInfo()); // Logout success message
|
||||
|
||||
// Change locale again
|
||||
infoPage.openLanguage("Čeština");
|
||||
Assert.assertEquals("Odhlášení bylo úspěšné", infoPage.getInfo()); // Logout success message
|
||||
|
||||
// Refresh page
|
||||
driver.navigate().refresh();
|
||||
Assert.assertEquals("Odhlášení bylo úspěšné", infoPage.getInfo()); // Logout success message
|
||||
|
||||
infoPage.clickBackToApplicationLinkCs();
|
||||
WaitUtils.waitForPageToLoad();
|
||||
MatcherAssert.assertThat(driver.getCurrentUrl(), endsWith("/app/auth"));
|
||||
|
|
Loading…
Reference in a new issue