KEYCLOAK-9632 Improve handling of user locale
This commit is contained in:
parent
4b09a4a2af
commit
42773592ca
24 changed files with 380 additions and 352 deletions
|
@ -215,13 +215,6 @@ declare namespace Keycloak {
|
||||||
*/
|
*/
|
||||||
locale?: string;
|
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).
|
* Specifies arguments that are passed to the Cordova in-app-browser (if applicable).
|
||||||
* Options 'hidden' and 'location' are not affected by these arguments.
|
* Options 'hidden' and 'location' are not affected by these arguments.
|
||||||
|
|
|
@ -439,10 +439,6 @@
|
||||||
url += '&ui_locales=' + encodeURIComponent(options.locale);
|
url += '&ui_locales=' + encodeURIComponent(options.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options && options.kcLocale) {
|
|
||||||
url += '&kc_locale=' + encodeURIComponent(options.kcLocale);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kc.pkceMethod) {
|
if (kc.pkceMethod) {
|
||||||
var codeVerifier = generateCodeVerifier(96);
|
var codeVerifier = generateCodeVerifier(96);
|
||||||
callbackState.pkceCodeVerifier = codeVerifier;
|
callbackState.pkceCodeVerifier = codeVerifier;
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
import org.keycloak.models.utils.DefaultRequiredActions;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
|
||||||
|
@ -58,6 +59,7 @@ public class MigrateTo9_0_0 implements Migration {
|
||||||
addAccountConsoleClient(realm);
|
addAccountConsoleClient(realm);
|
||||||
addAccountApiRoles(realm);
|
addAccountApiRoles(realm);
|
||||||
enablePkceAdminAccountClients(realm);
|
enablePkceAdminAccountClients(realm);
|
||||||
|
DefaultRequiredActions.addUpdateLocaleAction(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addAccountApiRoles(RealmModel realm) {
|
private void addAccountApiRoles(RealmModel realm) {
|
||||||
|
|
|
@ -83,6 +83,19 @@ public class DefaultRequiredActions {
|
||||||
realm.addRequiredActionProvider(termsAndConditions);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,29 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public interface LocaleSelectorProvider extends Provider {
|
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
|
* Resolve the locale which should be used for the request
|
||||||
|
*
|
||||||
* @param user
|
* @param user
|
||||||
* @return
|
* @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);
|
||||||
|
|
||||||
}
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,152 +17,207 @@
|
||||||
package org.keycloak.locale;
|
package org.keycloak.locale;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.util.CookieHelper;
|
import org.keycloak.services.util.CookieHelper;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Cookie;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class DefaultLocaleSelectorProvider implements LocaleSelectorProvider {
|
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";
|
private KeycloakSession session;
|
||||||
protected static final String KC_LOCALE_PARAM = "kc_locale";
|
|
||||||
|
|
||||||
protected final KeycloakSession session;
|
|
||||||
|
|
||||||
public DefaultLocaleSelectorProvider(KeycloakSession session) {
|
public DefaultLocaleSelectorProvider(KeycloakSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Locale resolveLocale(RealmModel realm, UserModel user) {
|
public Locale resolveLocale(UserModel user) {
|
||||||
final HttpHeaders requestHeaders = session.getContext().getRequestHeaders();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
final UriInfo uri = session.getContext().getUri();
|
HttpHeaders requestHeaders = session.getContext().getRequestHeaders();
|
||||||
return getLocale(realm, user, requestHeaders, uri);
|
AuthenticationSessionModel session = this.session.getContext().getAuthenticationSession();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Locale getLocale(RealmModel realm, UserModel user, HttpHeaders requestHeaders, UriInfo uriInfo) {
|
|
||||||
if (!realm.isInternationalizationEnabled()) {
|
if (!realm.isInternationalizationEnabled()) {
|
||||||
return Locale.ENGLISH;
|
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) {
|
public void updateUsersLocale(UserModel user, String locale) {
|
||||||
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) {
|
|
||||||
if (!locale.equals(user.getFirstAttribute("locale"))) {
|
if (!locale.equals(user.getFirstAttribute("locale"))) {
|
||||||
try {
|
try {
|
||||||
user.setSingleAttribute(UserModel.LOCALE, locale);
|
user.setSingleAttribute(UserModel.LOCALE, locale);
|
||||||
|
updateLocaleCookie(session.getContext().getRealm(), locale, session.getContext().getUri());
|
||||||
} catch (ReadOnlyException e) {
|
} catch (ReadOnlyException e) {
|
||||||
logger.debug("Attempt to store 'locale' attribute to read only user model. Ignoring exception", 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() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.locale.LocaleSelectorProvider;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
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.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
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.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
|
||||||
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
|
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
|
||||||
if (request.getDisplay() != null) authenticationSession.setAuthNote(OAuth2Constants.DISPLAY, request.getDisplay());
|
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
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
|
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
|
||||||
|
|
|
@ -41,6 +41,7 @@ public class AuthorizationEndpointRequest {
|
||||||
String idpHint;
|
String idpHint;
|
||||||
String action;
|
String action;
|
||||||
String claims;
|
String claims;
|
||||||
|
String uiLocales;
|
||||||
Map<String, String> additionalReqParams = new HashMap<>();
|
Map<String, String> additionalReqParams = new HashMap<>();
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc7636#section-6.1
|
// https://tools.ietf.org/html/rfc7636#section-6.1
|
||||||
|
@ -126,4 +127,8 @@ public class AuthorizationEndpointRequest {
|
||||||
public String getInvalidRequestMessage() {
|
public String getInvalidRequestMessage() {
|
||||||
return invalidRequestMessage;
|
return invalidRequestMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUiLocales() {
|
||||||
|
return uiLocales;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,6 +94,7 @@ abstract class AuthzEndpointRequestParser {
|
||||||
request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
|
request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
|
||||||
request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM));
|
request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM));
|
||||||
request.display = replaceIfNotNull(request.display, getParameter(OAuth2Constants.DISPLAY));
|
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
|
// https://tools.ietf.org/html/rfc7636#section-6.1
|
||||||
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
|
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
|
||||||
|
|
|
@ -129,7 +129,7 @@ public class DefaultKeycloakContext implements KeycloakContext {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Locale resolveLocale(UserModel user) {
|
public Locale resolveLocale(UserModel user) {
|
||||||
return session.getProvider(LocaleSelectorProvider.class).resolveLocale(realm, user);
|
return session.getProvider(LocaleSelectorProvider.class).resolveLocale(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.exceptions.TokenNotActiveException;
|
import org.keycloak.exceptions.TokenNotActiveException;
|
||||||
|
import org.keycloak.locale.LocaleSelectorProvider;
|
||||||
import org.keycloak.models.ActionTokenKeyModel;
|
import org.keycloak.models.ActionTokenKeyModel;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -258,9 +259,22 @@ public class LoginActionsService {
|
||||||
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
||||||
boolean actionRequest = checks.isActionRequest();
|
boolean actionRequest = checks.isActionRequest();
|
||||||
|
|
||||||
|
processLocaleParam(authSession);
|
||||||
|
|
||||||
return processAuthentication(actionRequest, execution, authSession, null);
|
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) {
|
protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) {
|
||||||
return processFlow(action, execution, authSession, AUTHENTICATE_PATH, AuthenticationFlowResolver.resolveBrowserFlow(authSession), errorMessage, new AuthenticationProcessor());
|
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) {
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
ClientModel client = realm.getClientByClientId(clientId);
|
ClientModel client = realm.getClientByClientId(clientId);
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
|
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
|
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
|
||||||
if (authSession == null && code == null) {
|
if (authSession == null && code == null) {
|
||||||
|
@ -543,6 +558,8 @@ public class LoginActionsService {
|
||||||
|
|
||||||
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
||||||
tokenContext.setAuthenticationSession(authSession, true);
|
tokenContext.setAuthenticationSession(authSession, true);
|
||||||
|
|
||||||
|
processLocaleParam(authSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
initLoginEvent(authSession);
|
initLoginEvent(authSession);
|
||||||
|
@ -678,6 +695,8 @@ public class LoginActionsService {
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
||||||
|
|
||||||
|
processLocaleParam(authSession);
|
||||||
|
|
||||||
AuthenticationManager.expireIdentityCookie(realm, session.getContext().getUri(), clientConnection);
|
AuthenticationManager.expireIdentityCookie(realm, session.getContext().getUri(), clientConnection);
|
||||||
|
|
||||||
return processRegistration(checks.isActionRequest(), execution, authSession, null);
|
return processRegistration(checks.isActionRequest(), execution, authSession, null);
|
||||||
|
@ -738,6 +757,8 @@ public class LoginActionsService {
|
||||||
event.detail(Details.CODE_ID, code);
|
event.detail(Details.CODE_ID, code);
|
||||||
final AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
final AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
||||||
|
|
||||||
|
processLocaleParam(authSession);
|
||||||
|
|
||||||
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
|
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
|
||||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, noteKey);
|
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, noteKey);
|
||||||
if (serializedCtx == null) {
|
if (serializedCtx == null) {
|
||||||
|
@ -761,7 +782,6 @@ public class LoginActionsService {
|
||||||
event.detail(Details.IDENTITY_PROVIDER, identityProviderAlias)
|
event.detail(Details.IDENTITY_PROVIDER, identityProviderAlias)
|
||||||
.detail(Details.IDENTITY_PROVIDER_USERNAME, brokerContext.getUsername());
|
.detail(Details.IDENTITY_PROVIDER_USERNAME, brokerContext.getUsername());
|
||||||
|
|
||||||
|
|
||||||
AuthenticationProcessor processor = new AuthenticationProcessor() {
|
AuthenticationProcessor processor = new AuthenticationProcessor() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -956,6 +976,9 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
AuthenticationSessionModel authSession = checks.getAuthenticationSession();
|
||||||
|
|
||||||
|
processLocaleParam(authSession);
|
||||||
|
|
||||||
if (!checks.isActionRequest()) {
|
if (!checks.isActionRequest()) {
|
||||||
initLoginEvent(authSession);
|
initLoginEvent(authSession);
|
||||||
event.event(EventType.CUSTOM_REQUIRED_ACTION);
|
event.event(EventType.CUSTOM_REQUIRED_ACTION);
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.keycloak.events.EventType;
|
||||||
import org.keycloak.forms.account.AccountPages;
|
import org.keycloak.forms.account.AccountPages;
|
||||||
import org.keycloak.forms.account.AccountProvider;
|
import org.keycloak.forms.account.AccountProvider;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
|
import org.keycloak.locale.LocaleSelectorProvider;
|
||||||
import org.keycloak.models.AccountRoles;
|
import org.keycloak.models.AccountRoles;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
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);
|
return account.createResponse(page);
|
||||||
} else {
|
} else {
|
||||||
return login(path);
|
return login(path);
|
||||||
|
|
|
@ -920,6 +920,7 @@ public class AuthenticationManagementResource {
|
||||||
public static RequiredActionProviderRepresentation toRepresentation(RequiredActionProviderModel model) {
|
public static RequiredActionProviderRepresentation toRepresentation(RequiredActionProviderModel model) {
|
||||||
RequiredActionProviderRepresentation rep = new RequiredActionProviderRepresentation();
|
RequiredActionProviderRepresentation rep = new RequiredActionProviderRepresentation();
|
||||||
rep.setAlias(model.getAlias());
|
rep.setAlias(model.getAlias());
|
||||||
|
rep.setProviderId(model.getProviderId());
|
||||||
rep.setName(model.getName());
|
rep.setName(model.getName());
|
||||||
rep.setDefaultAction(model.isDefaultAction());
|
rep.setDefaultAction(model.isDefaultAction());
|
||||||
rep.setPriority(model.getPriority());
|
rep.setPriority(model.getPriority());
|
||||||
|
|
|
@ -22,3 +22,4 @@ org.keycloak.authentication.requiredactions.VerifyEmail
|
||||||
org.keycloak.authentication.requiredactions.TermsAndConditions
|
org.keycloak.authentication.requiredactions.TermsAndConditions
|
||||||
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
||||||
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory
|
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory
|
||||||
|
org.keycloak.authentication.requiredactions.UpdateUserLocaleAction
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -49,6 +49,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
|
||||||
addRequiredAction(expected, "UPDATE_PROFILE", "Update Profile", true, false, null);
|
addRequiredAction(expected, "UPDATE_PROFILE", "Update Profile", true, false, null);
|
||||||
addRequiredAction(expected, "VERIFY_EMAIL", "Verify Email", 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, "terms_and_conditions", "Terms and Conditions", false, false, null);
|
||||||
|
addRequiredAction(expected, "update_user_locale", "Update User Locale", true, false, null);
|
||||||
|
|
||||||
compareRequiredActions(expected, sort(result));
|
compareRequiredActions(expected, sort(result));
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,9 @@ public class EmailTest extends AbstractI18NTest {
|
||||||
|
|
||||||
String link = MailUtils.getPasswordResetEmailLink(greenMail.getLastReceivedMessage());
|
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);
|
DroneUtils.getCurrentDriver().navigate().to(link);
|
||||||
WaitUtils.waitForPageToLoad();
|
WaitUtils.waitForPageToLoad();
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.adapters.HttpClientBuilder;
|
import org.keycloak.adapters.HttpClientBuilder;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
|
import org.keycloak.locale.LocaleSelectorProvider;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
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.LoginPasswordUpdatePage;
|
||||||
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
||||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||||
|
import org.openqa.selenium.Cookie;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
|
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
|
||||||
|
@ -197,6 +199,49 @@ public class LoginPageTest extends AbstractI18NTest {
|
||||||
oauth.clientId("test-app");
|
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) {
|
private void switchLanguageToGermanAndBack(String expectedEnglishMessage, String expectedGermanMessage, LanguageComboboxAwarePage page) {
|
||||||
// Switch language to Deutsch
|
// Switch language to Deutsch
|
||||||
|
|
|
@ -133,32 +133,6 @@ public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
||||||
.init(defaultArguments(), this::assertInitNotAuth);
|
.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
|
@Test
|
||||||
public void testLoginWithPkceS256() {
|
public void testLoginWithPkceS256() {
|
||||||
JSObjectBuilder pkceS256 = defaultArguments().pkceS256();
|
JSObjectBuilder pkceS256 = defaultArguments().pkceS256();
|
||||||
|
|
|
@ -79,14 +79,10 @@ import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
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.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
|
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
|
||||||
|
@ -284,6 +280,8 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
|
||||||
testAccountClient(migrationRealm);
|
testAccountClient(migrationRealm);
|
||||||
testAdminClientPkce(masterRealm);
|
testAdminClientPkce(masterRealm);
|
||||||
testAdminClientPkce(migrationRealm);
|
testAdminClientPkce(migrationRealm);
|
||||||
|
testUserLocaleActionAdded(masterRealm);
|
||||||
|
testUserLocaleActionAdded(migrationRealm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testAccountClient(RealmResource realm) {
|
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());
|
log.info("Taking required actions from realm: " + realm.toRepresentation().getRealm());
|
||||||
List<RequiredActionProviderRepresentation> actions = realm.flows().getRequiredActions();
|
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
|
// Checking the priority
|
||||||
int priority = 10;
|
int priority = 10;
|
||||||
for (RequiredActionProviderRepresentation action : actions) {
|
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;
|
priority += 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -817,6 +813,18 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
|
||||||
Assert.assertEquals(subflowExecution.getLevel() + 1, childEx2.getLevel());
|
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 {
|
protected void testMigrationTo2_x() throws Exception {
|
||||||
testMigrationTo2_0_0();
|
testMigrationTo2_0_0();
|
||||||
testMigrationTo2_1_0();
|
testMigrationTo2_1_0();
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
var features = {
|
var features = {
|
||||||
isRegistrationEmailAsUsername : ${realm.registrationEmailAsUsername?c},
|
isRegistrationEmailAsUsername : ${realm.registrationEmailAsUsername?c},
|
||||||
isEditUserNameAllowed : ${realm.editUsernameAllowed?c},
|
isEditUserNameAllowed : ${realm.editUsernameAllowed?c},
|
||||||
isInternationalizationEnabled : ${realm.internationalizationEnabled?c},
|
isInternationalizationEnabled : false,
|
||||||
isLinkedAccountsEnabled : ${realm.identityFederationEnabled?c},
|
isLinkedAccountsEnabled : ${realm.identityFederationEnabled?c},
|
||||||
isEventsEnabled : ${isEventsEnabled?c},
|
isEventsEnabled : ${isEventsEnabled?c},
|
||||||
isMyResourcesEnabled : ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
|
isMyResourcesEnabled : ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
|
||||||
|
@ -155,28 +155,6 @@
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#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">
|
<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="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>
|
<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>
|
</li>
|
||||||
</#if>
|
</#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">
|
<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>
|
<a href="#" onclick="keycloak.login();" role="menuitem" tabindex="0" aria-disabled="false" class="pf-c-dropdown__menu-item">${msg("doLogIn")}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in a new issue