KEYCLOAK-9632 Improve handling of user locale

This commit is contained in:
stianst 2020-02-06 14:23:44 +01:00 committed by Stian Thorgersen
parent 4b09a4a2af
commit 42773592ca
24 changed files with 380 additions and 352 deletions

View file

@ -215,13 +215,6 @@ declare namespace Keycloak {
*/
locale?: string;
/**
* Specifies the desired Keycloak locale for the UI. This differs from
* the locale param in that it tells the Keycloak server to set a cookie and update
* the user's profile to a new preferred locale.
*/
kcLocale?: string;
/**
* Specifies arguments that are passed to the Cordova in-app-browser (if applicable).
* Options 'hidden' and 'location' are not affected by these arguments.

View file

@ -439,10 +439,6 @@
url += '&ui_locales=' + encodeURIComponent(options.locale);
}
if (options && options.kcLocale) {
url += '&kc_locale=' + encodeURIComponent(options.kcLocale);
}
if (kc.pkceMethod) {
var codeVerifier = generateCodeVerifier(96);
callbackState.pkceCodeVerifier = codeVerifier;

View file

@ -26,6 +26,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.DefaultRequiredActions;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.RealmRepresentation;
@ -58,6 +59,7 @@ public class MigrateTo9_0_0 implements Migration {
addAccountConsoleClient(realm);
addAccountApiRoles(realm);
enablePkceAdminAccountClients(realm);
DefaultRequiredActions.addUpdateLocaleAction(realm);
}
private void addAccountApiRoles(RealmModel realm) {

View file

@ -83,6 +83,19 @@ public class DefaultRequiredActions {
realm.addRequiredActionProvider(termsAndConditions);
}
addUpdateLocaleAction(realm);
}
public static void addUpdateLocaleAction(RealmModel realm) {
if (realm.getRequiredActionProviderByAlias("update_user_locale") == null) {
RequiredActionProviderModel updateUserLocale = new RequiredActionProviderModel();
updateUserLocale.setEnabled(true);
updateUserLocale.setAlias("update_user_locale");
updateUserLocale.setName("Update User Locale");
updateUserLocale.setProviderId("update_user_locale");
updateUserLocale.setDefaultAction(false);
updateUserLocale.setPriority(1000);
realm.addRequiredActionProvider(updateUserLocale);
}
}
}

View file

@ -21,13 +21,29 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import javax.ws.rs.core.UriInfo;
import java.util.Locale;
public interface LocaleSelectorProvider extends Provider {
String LOCALE_COOKIE = "KEYCLOAK_LOCALE";
String KC_LOCALE_PARAM = "kc_locale";
String CLIENT_REQUEST_LOCALE = "locale_client_requested";
String USER_REQUEST_LOCALE = "locale_user_requested";
/**
* Resolve the locale which should be used for the request
*
* @param user
* @return
*/
Locale resolveLocale(RealmModel realm, UserModel user);
Locale resolveLocale(UserModel user);
void updateUsersLocale(UserModel user, String locale);
void updateLocaleCookie(RealmModel realm, String locale, UriInfo uriInfo);
void expireLocaleCookie(RealmModel realm, UriInfo uriInfo);
}

View file

@ -0,0 +1,66 @@
package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.locale.LocaleSelectorProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel;
public class UpdateUserLocaleAction implements RequiredActionProvider, RequiredActionFactory {
@Override
public String getDisplayText() {
return "Update User Locale";
}
@Override
public void evaluateTriggers(RequiredActionContext context) {
String userRequestedLocale = context.getAuthenticationSession().getAuthNote(LocaleSelectorProvider.USER_REQUEST_LOCALE);
if (userRequestedLocale != null) {
LocaleSelectorProvider provider = context.getSession().getProvider(LocaleSelectorProvider.class);
provider.updateUsersLocale(context.getUser(), userRequestedLocale);
} else {
String userLocale = context.getUser().getFirstAttribute(UserModel.LOCALE);
LocaleSelectorProvider provider = context.getSession().getProvider(LocaleSelectorProvider.class);
if (userLocale != null) {
provider.updateLocaleCookie(context.getRealm(), userLocale, context.getUriInfo());
} else {
provider.expireLocaleCookie(context.getRealm(), context.getUriInfo());
}
}
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
}
@Override
public void processAction(RequiredActionContext context) {
}
@Override
public RequiredActionProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "update_user_locale";
}
}

View file

@ -17,152 +17,207 @@
package org.keycloak.locale;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.storage.ReadOnlyException;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriInfo;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public class DefaultLocaleSelectorProvider implements LocaleSelectorProvider {
private static final Logger logger = Logger.getLogger(DefaultLocaleSelectorProvider.class);
private static final Logger logger = Logger.getLogger(LocaleSelectorProvider.class);
protected static final String LOCALE_COOKIE = "KEYCLOAK_LOCALE";
protected static final String KC_LOCALE_PARAM = "kc_locale";
protected final KeycloakSession session;
private KeycloakSession session;
public DefaultLocaleSelectorProvider(KeycloakSession session) {
this.session = session;
}
@Override
public Locale resolveLocale(RealmModel realm, UserModel user) {
final HttpHeaders requestHeaders = session.getContext().getRequestHeaders();
final UriInfo uri = session.getContext().getUri();
return getLocale(realm, user, requestHeaders, uri);
}
public Locale resolveLocale(UserModel user) {
RealmModel realm = session.getContext().getRealm();
HttpHeaders requestHeaders = session.getContext().getRequestHeaders();
AuthenticationSessionModel session = this.session.getContext().getAuthenticationSession();
@Override
public void close() {
}
protected Locale getLocale(RealmModel realm, UserModel user, HttpHeaders requestHeaders, UriInfo uriInfo) {
if (!realm.isInternationalizationEnabled()) {
return Locale.ENGLISH;
} else {
Locale locale = getUserLocale(realm, user, requestHeaders, uriInfo);
return locale != null ? locale : Locale.forLanguageTag(realm.getDefaultLocale());
}
Locale userLocale = getUserLocale(realm, session, user, requestHeaders);
if (userLocale != null) {
return userLocale;
}
String realmDefaultLocale = realm.getDefaultLocale();
if (realmDefaultLocale != null) {
return Locale.forLanguageTag(realmDefaultLocale);
}
return Locale.ENGLISH;
}
protected Locale getUserLocale(RealmModel realm, UserModel user, HttpHeaders requestHeaders, UriInfo uriInfo) {
final LocaleSelection kcLocaleQueryParamSelection = getKcLocaleQueryParamSelection(realm, uriInfo);
if (kcLocaleQueryParamSelection != null) {
updateLocaleCookie(realm, kcLocaleQueryParamSelection.getLocaleString(), uriInfo);
if (user != null) {
updateUsersLocale(user, kcLocaleQueryParamSelection.getLocaleString());
}
return kcLocaleQueryParamSelection.getLocale();
}
final LocaleSelection localeCookieSelection = getLocaleCookieSelection(realm, requestHeaders);
if (localeCookieSelection != null) {
if (user != null) {
updateUsersLocale(user, localeCookieSelection.getLocaleString());
}
return localeCookieSelection.getLocale();
}
final LocaleSelection userProfileSelection = getUserProfileSelection(realm, user);
if (userProfileSelection != null) {
updateLocaleCookie(realm, userProfileSelection.getLocaleString(), uriInfo);
return userProfileSelection.getLocale();
}
final LocaleSelection uiLocalesQueryParamSelection = getUiLocalesQueryParamSelection(realm, uriInfo);
if (uiLocalesQueryParamSelection != null) {
return uiLocalesQueryParamSelection.getLocale();
}
final LocaleSelection acceptLanguageHeaderSelection = getAcceptLanguageHeaderLocale(realm, requestHeaders);
if (acceptLanguageHeaderSelection != null) {
return acceptLanguageHeaderSelection.getLocale();
}
return null;
}
protected LocaleSelection getKcLocaleQueryParamSelection(RealmModel realm, UriInfo uriInfo) {
if (uriInfo == null || !uriInfo.getQueryParameters().containsKey(KC_LOCALE_PARAM)) {
return null;
}
String localeString = uriInfo.getQueryParameters().getFirst(KC_LOCALE_PARAM);
return findLocale(realm, localeString);
}
protected LocaleSelection getLocaleCookieSelection(RealmModel realm, HttpHeaders httpHeaders) {
if (httpHeaders == null || !httpHeaders.getCookies().containsKey(LOCALE_COOKIE)) {
return null;
}
String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue();
return findLocale(realm, localeString);
}
protected LocaleSelection getUserProfileSelection(RealmModel realm, UserModel user) {
if (user == null || !user.getAttributes().containsKey(UserModel.LOCALE)) {
return null;
}
String localeString = user.getFirstAttribute(UserModel.LOCALE);
return findLocale(realm, localeString);
}
protected LocaleSelection getUiLocalesQueryParamSelection(RealmModel realm, UriInfo uriInfo) {
if (uriInfo == null || !uriInfo.getQueryParameters().containsKey(OAuth2Constants.UI_LOCALES_PARAM)) {
return null;
}
String localeString = uriInfo.getQueryParameters().getFirst(OAuth2Constants.UI_LOCALES_PARAM);
return findLocale(realm, localeString.split(" "));
}
protected LocaleSelection getAcceptLanguageHeaderLocale(RealmModel realm, HttpHeaders httpHeaders) {
if (httpHeaders == null || httpHeaders.getAcceptableLanguages() == null || httpHeaders.getAcceptableLanguages().isEmpty()) {
return null;
}
for (Locale l : httpHeaders.getAcceptableLanguages()) {
String localeString = l.toLanguageTag();
LocaleSelection localeSelection = findLocale(realm, localeString);
if (localeSelection != null) {
return localeSelection;
}
}
return null;
}
protected void updateLocaleCookie(RealmModel realm, String locale, UriInfo uriInfo) {
boolean secure = realm.getSslRequired().isRequired(uriInfo.getRequestUri().getHost());
CookieHelper.addCookie(LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, secure, true);
}
protected LocaleSelection findLocale(RealmModel realm, String... localeStrings) {
return new LocaleNegotiator(realm.getSupportedLocales()).invoke(localeStrings);
}
protected void updateUsersLocale(UserModel user, String locale) {
public void updateUsersLocale(UserModel user, String locale) {
if (!locale.equals(user.getFirstAttribute("locale"))) {
try {
user.setSingleAttribute(UserModel.LOCALE, locale);
updateLocaleCookie(session.getContext().getRealm(), locale, session.getContext().getUri());
} catch (ReadOnlyException e) {
logger.debug("Attempt to store 'locale' attribute to read only user model. Ignoring exception", e);
}
}
logger.debugv("Setting locale for user {0} to {1}", user.getUsername(), locale);
}
public void updateLocaleCookie(RealmModel realm, String locale, UriInfo uriInfo) {
boolean secure = realm.getSslRequired().isRequired(uriInfo.getRequestUri().getHost());
CookieHelper.addCookie(LocaleSelectorProvider.LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, secure, true);
logger.debugv("Updating locale cookie to {0}", locale);
}
public void expireLocaleCookie(RealmModel realm, UriInfo uriInfo) {
boolean secure = realm.getSslRequired().isRequired(uriInfo.getRequestUri().getHost());
CookieHelper.addCookie(LocaleSelectorProvider.LOCALE_COOKIE, "", AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, "Expiring cookie", 0, secure, true);
}
private Locale getUserLocale(RealmModel realm, AuthenticationSessionModel session, UserModel user, HttpHeaders requestHeaders) {
Locale locale;
locale = getUserSelectedLocale(realm, session);
if (locale != null) {
return locale;
}
locale = getUserProfileSelection(realm, user);
if (locale != null) {
return locale;
}
locale = getClientSelectedLocale(realm, session);
if (locale != null) {
return locale;
}
locale = getLocaleCookieSelection(realm, requestHeaders);
if (locale != null) {
return locale;
}
locale = getAcceptLanguageHeaderLocale(realm, requestHeaders);
if (locale != null) {
return locale;
}
return null;
}
private Locale getUserSelectedLocale(RealmModel realm, AuthenticationSessionModel session) {
if (session == null) {
return null;
}
String locale = session.getAuthNote(USER_REQUEST_LOCALE);
if (locale == null) {
return null;
}
return findLocale(realm, locale);
}
private Locale getUserProfileSelection(RealmModel realm, UserModel user) {
if (user == null) {
return null;
}
String locale = user.getFirstAttribute(UserModel.LOCALE);
if (locale == null) {
return null;
}
return findLocale(realm, locale);
}
private Locale getClientSelectedLocale(RealmModel realm, AuthenticationSessionModel session) {
if (session == null) {
return null;
}
String locale = session.getAuthNote(LocaleSelectorProvider.CLIENT_REQUEST_LOCALE);
if (locale == null) {
return null;
}
return findLocale(realm, locale.split(" "));
}
private Locale getLocaleCookieSelection(RealmModel realm, HttpHeaders httpHeaders) {
if (httpHeaders == null) {
return null;
}
Cookie localeCookie = httpHeaders.getCookies().get(LOCALE_COOKIE);
if (localeCookie == null) {
return null;
}
return findLocale(realm, localeCookie.getValue());
}
private Locale getAcceptLanguageHeaderLocale(RealmModel realm, HttpHeaders httpHeaders) {
if (httpHeaders == null) {
return null;
}
List<Locale> acceptableLanguages = httpHeaders.getAcceptableLanguages();
if (acceptableLanguages == null || acceptableLanguages.isEmpty()) {
return null;
}
for (Locale l : acceptableLanguages) {
Locale locale = findLocale(realm, l.toLanguageTag());
if (locale != null) {
return locale;
}
}
return null;
}
private Locale findLocale(RealmModel realm, String... localeStrings) {
Set<String> supportedLocales = realm.getSupportedLocales();
for (String localeString : localeStrings) {
if (localeString != null) {
Locale result = null;
Locale search = Locale.forLanguageTag(localeString);
for (String languageTag : supportedLocales) {
Locale locale = Locale.forLanguageTag(languageTag);
if (locale.getLanguage().equals(search.getLanguage())) {
if (search.getCountry().equals("") ^ locale.getCountry().equals("") && result == null) {
result = locale;
}
if (locale.getCountry().equals(search.getCountry())) {
return locale;
}
}
}
if (result != null) {
return result;
}
}
}
return null;
}
@Override
public void close() {
}
}

View file

@ -1,53 +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.locale;
import java.util.Locale;
import java.util.Set;
public class LocaleNegotiator {
private Set<String> supportedLocales;
public LocaleNegotiator(Set<String> supportedLocales) {
this.supportedLocales = supportedLocales;
}
public LocaleSelection invoke(String... localeStrings) {
for (String localeString : localeStrings) {
if (localeString != null) {
Locale result = null;
Locale search = Locale.forLanguageTag(localeString);
for (String languageTag : supportedLocales) {
Locale locale = Locale.forLanguageTag(languageTag);
if (locale.getLanguage().equals(search.getLanguage())) {
if (search.getCountry().equals("") ^ locale.getCountry().equals("") && result == null) {
result = locale;
}
if (locale.getCountry().equals(search.getCountry())) {
return new LocaleSelection(localeString, locale);
}
}
}
if (result != null) {
return new LocaleSelection(localeString, result);
}
}
}
return null;
}
}

View file

@ -1,37 +0,0 @@
/*
* Copyright 2018 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.locale;
import java.util.Locale;
public class LocaleSelection {
private final String localeString;
private final Locale locale;
public LocaleSelection(String localeString, Locale locale) {
this.localeString = localeString;
this.locale = locale;
}
public String getLocaleString() {
return localeString;
}
public Locale getLocale() {
return locale;
}
}

View file

@ -26,6 +26,7 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.locale.LocaleSelectorProvider;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
@ -55,7 +56,6 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -444,6 +444,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
if (request.getDisplay() != null) authenticationSession.setAuthNote(OAuth2Constants.DISPLAY, request.getDisplay());
if (request.getUiLocales() != null) authenticationSession.setAuthNote(LocaleSelectorProvider.CLIENT_REQUEST_LOCALE, request.getUiLocales());
// https://tools.ietf.org/html/rfc7636#section-4
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());

View file

@ -41,6 +41,7 @@ public class AuthorizationEndpointRequest {
String idpHint;
String action;
String claims;
String uiLocales;
Map<String, String> additionalReqParams = new HashMap<>();
// https://tools.ietf.org/html/rfc7636#section-6.1
@ -126,4 +127,8 @@ public class AuthorizationEndpointRequest {
public String getInvalidRequestMessage() {
return invalidRequestMessage;
}
public String getUiLocales() {
return uiLocales;
}
}

View file

@ -94,6 +94,7 @@ abstract class AuthzEndpointRequestParser {
request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM));
request.display = replaceIfNotNull(request.display, getParameter(OAuth2Constants.DISPLAY));
request.uiLocales = replaceIfNotNull(request.uiLocales, getParameter(OAuth2Constants.UI_LOCALES_PARAM));
// https://tools.ietf.org/html/rfc7636#section-6.1
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));

View file

@ -129,7 +129,7 @@ public class DefaultKeycloakContext implements KeycloakContext {
@Override
public Locale resolveLocale(UserModel user) {
return session.getProvider(LocaleSelectorProvider.class).resolveLocale(realm, user);
return session.getProvider(LocaleSelectorProvider.class).resolveLocale(user);
}
@Override

View file

@ -47,6 +47,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.exceptions.TokenNotActiveException;
import org.keycloak.locale.LocaleSelectorProvider;
import org.keycloak.models.ActionTokenKeyModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
@ -258,9 +259,22 @@ public class LoginActionsService {
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
boolean actionRequest = checks.isActionRequest();
processLocaleParam(authSession);
return processAuthentication(actionRequest, execution, authSession, null);
}
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);
LocaleSelectorProvider localeSelectorProvider = session.getProvider(LocaleSelectorProvider.class);
localeSelectorProvider.updateLocaleCookie(realm, locale, session.getContext().getUri());
}
}
}
protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
return processFlow(action, execution, authSession, AUTHENTICATE_PATH, AuthenticationFlowResolver.resolveBrowserFlow(authSession), errorMessage, new AuthenticationProcessor());
}
@ -356,6 +370,7 @@ public class LoginActionsService {
@QueryParam(Constants.TAB_ID) String tabId) {
ClientModel client = realm.getClientByClientId(clientId);
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
processLocaleParam(authSession);
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
if (authSession == null && code == null) {
@ -543,6 +558,8 @@ public class LoginActionsService {
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
tokenContext.setAuthenticationSession(authSession, true);
processLocaleParam(authSession);
}
initLoginEvent(authSession);
@ -678,6 +695,8 @@ public class LoginActionsService {
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
processLocaleParam(authSession);
AuthenticationManager.expireIdentityCookie(realm, session.getContext().getUri(), clientConnection);
return processRegistration(checks.isActionRequest(), execution, authSession, null);
@ -738,6 +757,8 @@ public class LoginActionsService {
event.detail(Details.CODE_ID, code);
final AuthenticationSessionModel authSession = checks.getAuthenticationSession();
processLocaleParam(authSession);
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, noteKey);
if (serializedCtx == null) {
@ -761,7 +782,6 @@ public class LoginActionsService {
event.detail(Details.IDENTITY_PROVIDER, identityProviderAlias)
.detail(Details.IDENTITY_PROVIDER_USERNAME, brokerContext.getUsername());
AuthenticationProcessor processor = new AuthenticationProcessor() {
@Override
@ -956,6 +976,9 @@ public class LoginActionsService {
}
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
processLocaleParam(authSession);
if (!checks.isActionRequest()) {
initLoginEvent(authSession);
event.event(EventType.CUSTOM_REQUIRED_ACTION);

View file

@ -39,6 +39,7 @@ import org.keycloak.events.EventType;
import org.keycloak.forms.account.AccountPages;
import org.keycloak.forms.account.AccountProvider;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.locale.LocaleSelectorProvider;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
@ -221,6 +222,12 @@ public class AccountFormService extends AbstractSecuredLocalService {
}
}
String locale = session.getContext().getUri().getQueryParameters().getFirst(LocaleSelectorProvider.KC_LOCALE_PARAM);
if (locale != null) {
LocaleSelectorProvider localeSelectorProvider = session.getProvider(LocaleSelectorProvider.class);
localeSelectorProvider.updateUsersLocale(auth.getUser(), locale);
}
return account.createResponse(page);
} else {
return login(path);

View file

@ -920,6 +920,7 @@ public class AuthenticationManagementResource {
public static RequiredActionProviderRepresentation toRepresentation(RequiredActionProviderModel model) {
RequiredActionProviderRepresentation rep = new RequiredActionProviderRepresentation();
rep.setAlias(model.getAlias());
rep.setProviderId(model.getProviderId());
rep.setName(model.getName());
rep.setDefaultAction(model.isDefaultAction());
rep.setPriority(model.getPriority());

View file

@ -22,3 +22,4 @@ org.keycloak.authentication.requiredactions.VerifyEmail
org.keycloak.authentication.requiredactions.TermsAndConditions
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory
org.keycloak.authentication.requiredactions.UpdateUserLocaleAction

View file

@ -1,57 +0,0 @@
package org.keycloak.locale;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
public class LocaleNegotiatorTest {
private LocaleNegotiator localeNegotiator;
@Before
public void setUp() {
Set<String> supportedLocales = new HashSet<>();
supportedLocales.add("de");
supportedLocales.add("de-AT");
supportedLocales.add("de-CH");
supportedLocales.add("de-DE");
supportedLocales.add("pt-BR");
localeNegotiator = new LocaleNegotiator(supportedLocales);
}
@Test
public void shouldMatchWithoutCountryCode() {
String expectedLocaleString = "de";
LocaleSelection actualLocale = localeNegotiator.invoke(expectedLocaleString);
Assert.assertEquals(Locale.GERMAN, actualLocale.getLocale());
Assert.assertEquals(expectedLocaleString, actualLocale.getLocaleString());
}
@Test
public void shouldMatchWithPriorityCountryCode() {
String expectedLocaleString = "de-CH";
LocaleSelection actualLocale = localeNegotiator.invoke(expectedLocaleString, "de");
Assert.assertEquals(new Locale("de", "CH"), actualLocale.getLocale());
Assert.assertEquals(expectedLocaleString, actualLocale.getLocaleString());
}
@Test
public void shouldMatchWithPriorityNoCountryCode() {
String expectedLocaleString = "de";
LocaleSelection actualLocale = localeNegotiator.invoke(expectedLocaleString, "de-CH");
Assert.assertEquals(new Locale(expectedLocaleString), actualLocale.getLocale());
Assert.assertEquals(expectedLocaleString, actualLocale.getLocaleString());
}
@Test
public void shouldMatchOmittedCountryCodeWithBestFit() {
String expectedLocaleString = "pt";
LocaleSelection actualLocale = localeNegotiator.invoke(expectedLocaleString, "es-ES");
Assert.assertEquals(new Locale("pt", "BR"), actualLocale.getLocale());
Assert.assertEquals(expectedLocaleString, actualLocale.getLocaleString());
}
}

View file

@ -49,6 +49,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
addRequiredAction(expected, "UPDATE_PROFILE", "Update Profile", true, false, null);
addRequiredAction(expected, "VERIFY_EMAIL", "Verify Email", true, false, null);
addRequiredAction(expected, "terms_and_conditions", "Terms and Conditions", false, false, null);
addRequiredAction(expected, "update_user_locale", "Update User Locale", true, false, null);
compareRequiredActions(expected, sort(result));

View file

@ -132,6 +132,9 @@ public class EmailTest extends AbstractI18NTest {
String link = MailUtils.getPasswordResetEmailLink(greenMail.getLastReceivedMessage());
// Make sure kc_locale added to link doesn't set locale
link += "&kc_locale=de";
DroneUtils.getCurrentDriver().navigate().to(link);
WaitUtils.waitForPageToLoad();

View file

@ -27,6 +27,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.locale.LocaleSelectorProvider;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@ -41,6 +42,7 @@ import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.openqa.selenium.Cookie;
/**
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
@ -197,6 +199,49 @@ public class LoginPageTest extends AbstractI18NTest {
oauth.clientId("test-app");
}
@Test
public void languageUserUpdates() {
ProfileAssume.assumeCommunity();
loginPage.open();
loginPage.openLanguage("Deutsch");
Assert.assertEquals("Deutsch", loginPage.getLanguageDropdownText());
Cookie localeCookie = driver.manage().getCookieNamed(LocaleSelectorProvider.LOCALE_COOKIE);
Assert.assertEquals("de", localeCookie.getValue());
loginPage.login("test-user@localhost", "password");
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
UserRepresentation userRep = user.toRepresentation();
Assert.assertEquals("de", userRep.getAttributes().get("locale").get(0));
appPage.logout();
loginPage.open();
Assert.assertEquals("Deutsch", loginPage.getLanguageDropdownText());
userRep.getAttributes().remove("locale");
user.update(userRep);
loginPage.open();
loginPage.login("test-user@localhost", "password");
// User locale should not be updated due to previous cookie
userRep = user.toRepresentation();
Assert.assertNull(userRep.getAttributes());
appPage.logout();
loginPage.open();
// Cookie should be removed as last user to login didn't have a locale
localeCookie = driver.manage().getCookieNamed(LocaleSelectorProvider.LOCALE_COOKIE);
Assert.assertNull(localeCookie);
}
private void switchLanguageToGermanAndBack(String expectedEnglishMessage, String expectedGermanMessage, LanguageComboboxAwarePage page) {
// Switch language to Deutsch

View file

@ -133,32 +133,6 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
.init(defaultArguments(), this::assertInitNotAuth);
}
@Test
public void testLoginWithKCLocale() {
ProfileAssume.assumeCommunity();
RealmRepresentation testRealmRep = testRealmResource().toRepresentation();
testRealmRep.setInternationalizationEnabled(true);
testRealmRep.setDefaultLocale("en");
testRealmRep.setSupportedLocales(Stream.of("en", "de").collect(Collectors.toSet()));
testRealmResource().update(testRealmRep);
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
.login(this::assertOnLoginPage)
.loginForm(testUser, this::assertOnTestAppUrl)
.init(defaultArguments(), this::assertSuccessfullyLoggedIn)
.logout(this::assertOnTestAppUrl)
.init(defaultArguments(), this::assertInitNotAuth)
.login("{kcLocale: 'de'}", assertLocaleIsSet("de"))
.loginForm(testUser, this::assertOnTestAppUrl)
.init(defaultArguments(), this::assertSuccessfullyLoggedIn)
.logout(this::assertOnTestAppUrl)
.init(defaultArguments(), this::assertInitNotAuth)
.login("{kcLocale: 'en'}", assertLocaleIsSet("en"));
}
@Test
public void testLoginWithPkceS256() {
JSObjectBuilder pkceS256 = defaultArguments().pkceS256();

View file

@ -79,14 +79,10 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
@ -284,6 +280,8 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
testAccountClient(migrationRealm);
testAdminClientPkce(masterRealm);
testAdminClientPkce(migrationRealm);
testUserLocaleActionAdded(masterRealm);
testUserLocaleActionAdded(migrationRealm);
}
private void testAccountClient(RealmResource realm) {
@ -724,17 +722,15 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
log.info("Taking required actions from realm: " + realm.toRepresentation().getRealm());
List<RequiredActionProviderRepresentation> actions = realm.flows().getRequiredActions();
// Checking if the actions are in alphabetical order
List<String> nameList = actions.stream().map(x -> x.getName()).collect(Collectors.toList());
log.debug("Obtained required actions: " + nameList);
List<String> sortedByName = nameList.stream().sorted().collect(Collectors.toList());
log.debug("Manually sorted required actions: " + sortedByName);
assertThat(nameList, is(equalTo(sortedByName)));
// Checking the priority
int priority = 10;
for (RequiredActionProviderRepresentation action : actions) {
assertThat(action.getPriority(), is(equalTo(priority)));
if (action.getAlias().equals("update_user_locale")) {
assertEquals(1000, action.getPriority());
} else {
assertEquals(priority, action.getPriority());
}
priority += 10;
}
}
@ -817,6 +813,18 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
Assert.assertEquals(subflowExecution.getLevel() + 1, childEx2.getLevel());
}
protected void testUserLocaleActionAdded(RealmResource realm) {
RequiredActionProviderRepresentation rep = realm.flows().getRequiredAction("update_user_locale");
assertNotNull(rep);
assertEquals("update_user_locale", rep.getAlias());
assertEquals("update_user_locale", rep.getProviderId());
assertEquals("Update User Locale", rep.getName());
assertEquals(1000, rep.getPriority());
assertTrue(rep.isEnabled());
assertFalse(rep.isDefaultAction());
}
protected void testMigrationTo2_x() throws Exception {
testMigrationTo2_0_0();
testMigrationTo2_1_0();

View file

@ -29,7 +29,7 @@
var features = {
isRegistrationEmailAsUsername : ${realm.registrationEmailAsUsername?c},
isEditUserNameAllowed : ${realm.editUsernameAllowed?c},
isInternationalizationEnabled : ${realm.internationalizationEnabled?c},
isInternationalizationEnabled : false,
isLinkedAccountsEnabled : ${realm.identityFederationEnabled?c},
isEventsEnabled : ${isEventsEnabled?c},
isMyResourcesEnabled : ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
@ -155,28 +155,6 @@
</div>
</#if>
<#if realm.internationalizationEnabled && supportedLocales?size gt 1>
<div class="pf-c-page__header-tools-group pf-m-icons">
<div id="landing-locale-dropdown" class="pf-c-dropdown">
<button onclick="toggleLocaleDropdown();" class="pf-c-dropdown__toggle pf-m-plain" id="landing-locale-dropdown-button" aria-expanded="false" aria-haspopup="true">
<span class="pf-c-dropdown__toggle-text">
${msg("locale_" + locale)}
</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul id="landing-locale-dropdown-list" class="pf-c-dropdown__menu" aria-labeledby="landing-locale-dropdown-button" role="menu" hidden>
<#list supportedLocales as locale, label>
<#if referrer?has_content && referrer_uri?has_content>
<li id="landing-locale-${locale}" role="none"><a href="${baseUrl}/?kc_locale=${locale}&referrer=${referrer}&referrer_uri=${referrer_uri}" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${label}</a></li>
<#else>
<li id="landing-locale-${locale}" role="none"><a href="${baseUrl}/?kc_locale=${locale}" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${label}</a></li>
</#if>
</#list>
</ul>
</div>
</div>
</#if>
<div class="pf-c-page__header-tools-group pf-m-icons">
<button id="landingSignInButton" tabindex="0" style="display:none" onclick="keycloak.login();" class="pf-c-button pf-m-primary" type="button">${msg("doLogIn")}</button>
<button id="landingSignOutButton" tabindex="0" style="display:none" onclick="keycloak.logout();" class="pf-c-button pf-m-primary" type="button">${msg("doSignOut")}</button>
@ -195,20 +173,6 @@
</li>
</#if>
<!-- locale selector for mobile -->
<#if realm.internationalizationEnabled && supportedLocales?size gt 1>
<li role="none" aria-expanded="false" onclick="toggleMobileChooseLocale();"><a href="#" id="landing-mobile-local-toggle" class="pf-c-dropdown__menu-item">${msg("locale_" + locale)} <i id="landingMobileLocaleSelectedIcon" class="fas fa-angle-right pf-c-options-menu__menu-item-icon" aria-hidden="true"></i></a></li>
<#list supportedLocales as locale, label>
<#if referrer?has_content && referrer_uri?has_content>
<li role="none" id="landing-mobile-locale-${locale}" style="display:none"><a href="${baseUrl}/?kc_locale=${locale}&referrer=${referrer}&referrer_uri=${referrer_uri}" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${label}</a></li>
<#else>
<li role="none" id="landing-mobile-locale-${locale}" style="display:none"><a href="${baseUrl}/?kc_locale=${locale}" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${label}</a></li>
</#if>
</#list>
<li id="landingMobileLocaleSeparator" class="pf-c-dropdown__separator" role="separator" style="display:none"></li>
</#if>
<!-- end locale selector for mobile -->
<li id="landingSignInLink" role="none" style="display:none">
<a href="#" onclick="keycloak.login();" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${msg("doLogIn")}</a>
</li>