From c85e0248ddc2fcdb8b0398d34fcf42eeca79282f Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Tue, 13 Oct 2015 15:58:31 +0200 Subject: [PATCH] KEYCLOAK-1883 Improve setting of users locale --- .../java/org/keycloak/OAuth2Constants.java | 3 + .../freemarker/FreeMarkerAccountProvider.java | 8 +- .../org/keycloak/freemarker/LocaleHelper.java | 140 ---------------- .../keycloak/freemarker/beans/LocaleBean.java | 3 +- .../keycloak/freemarke/LocaleHelperTest.java | 35 ---- forms/email-freemarker/pom.xml | 5 + .../freemarker/FreeMarkerEmailProvider.java | 3 +- .../FreeMarkerLoginFormsProvider.java | 12 +- .../org/keycloak/models/KeycloakContext.java | 3 + .../java/org/keycloak/models/UserModel.java | 11 +- .../services/DefaultKeycloakContext.java | 12 +- .../managers/AuthenticationManager.java | 38 +---- .../keycloak/services/util/LocaleHelper.java | 156 ++++++++++++++++++ .../org/keycloak/testsuite/OAuthClient.java | 4 +- .../keycloak/testsuite/i18n/EmailTest.java | 30 +++- .../testsuite/i18n/LoginPageTest.java | 5 +- 16 files changed, 220 insertions(+), 248 deletions(-) delete mode 100644 forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java delete mode 100644 forms/common-freemarker/src/test/java/org/keycloak/freemarke/LocaleHelperTest.java create mode 100644 services/src/main/java/org/keycloak/services/util/LocaleHelper.java diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java index 022dadd867..b552d1bd89 100644 --- a/core/src/main/java/org/keycloak/OAuth2Constants.java +++ b/core/src/main/java/org/keycloak/OAuth2Constants.java @@ -41,6 +41,9 @@ public interface OAuth2Constants { // http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess String OFFLINE_ACCESS = "offline_access"; + String UI_LOCALES_PARAM = "ui_locales"; + + } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java index f7825438ac..b285df628d 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java @@ -51,7 +51,6 @@ import org.keycloak.events.Event; import org.keycloak.freemarker.BrowserSecurityHeaderSetup; import org.keycloak.freemarker.FreeMarkerException; import org.keycloak.freemarker.FreeMarkerUtil; -import org.keycloak.freemarker.LocaleHelper; import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.ThemeProvider; import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod; @@ -65,7 +64,6 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.FormMessage; -import org.keycloak.services.Urls; /** * @author Stian Thorgersen @@ -130,7 +128,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { logger.warn("Failed to load properties", e); } - Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, headers); + Locale locale = session.getContext().resolveLocale(user); Properties messagesBundle; try { messagesBundle = theme.getMessages(locale); @@ -213,10 +211,6 @@ public class FreeMarkerAccountProvider implements AccountProvider { String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme); Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result); BrowserSecurityHeaderSetup.headers(builder, realm); - - String keycloakLocaleCookiePath = Urls.localeCookiePath(baseUri, realm.getName()); - - LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, keycloakLocaleCookiePath); return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java deleted file mode 100644 index 14f7f6c6aa..0000000000 --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @author tags. All rights reserved. - * - * 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.freemarker; - -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; - -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.NewCookie; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; -import java.util.*; - -/** - * @author Michael Gerber - */ -public class LocaleHelper { - public final static String LOCALE_COOKIE = "KEYCLOAK_LOCALE"; - public static final String UI_LOCALES_PARAM = "ui_locales"; - public static final String KC_LOCALE_PARAM = "kc_locale"; - - public static Locale getLocale(RealmModel realm, UserModel user) { - return getLocale(realm, user, null, null); - } - - public static Locale getLocale(RealmModel realm, UserModel user, UriInfo uriInfo, HttpHeaders httpHeaders) { - if(!realm.isInternationalizationEnabled()){ - return Locale.ENGLISH; - } - - //0. kc_locale query parameter - if(uriInfo != null && uriInfo.getQueryParameters().containsKey(KC_LOCALE_PARAM)){ - String localeString = uriInfo.getQueryParameters().getFirst(KC_LOCALE_PARAM); - Locale locale = findLocale(realm.getSupportedLocales(), localeString); - if(locale != null){ - if(user != null){ - user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag()); - } - return locale; - } - } - - //1. Locale cookie - if(httpHeaders != null && httpHeaders.getCookies().containsKey(LOCALE_COOKIE)){ - String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue(); - Locale locale = findLocale(realm.getSupportedLocales(), localeString); - if(locale != null){ - if(user != null && user.getFirstAttribute(UserModel.LOCALE) == null){ - user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag()); - } - return locale; - } - } - - //2. User profile - if(user != null && user.getAttributes().containsKey(UserModel.LOCALE)){ - String localeString = user.getFirstAttribute(UserModel.LOCALE); - Locale locale = findLocale(realm.getSupportedLocales(), localeString); - if(locale != null){ - - return locale; - } - } - - //3. ui_locales query parameter - if(uriInfo != null && uriInfo.getQueryParameters().containsKey(UI_LOCALES_PARAM)){ - String localeString = uriInfo.getQueryParameters().getFirst(UI_LOCALES_PARAM); - Locale locale = findLocale(realm.getSupportedLocales(), localeString.split(" ")); - if(locale != null){ - return locale; - } - } - - //4. Accept-Language http header - if(httpHeaders !=null && httpHeaders.getAcceptableLanguages() != null && !httpHeaders.getAcceptableLanguages().isEmpty()){ - for(Locale l : httpHeaders.getAcceptableLanguages()){ - String localeString = l.toLanguageTag(); - Locale locale = findLocale(realm.getSupportedLocales(), localeString); - if(locale != null){ - return locale; - } - } - } - - //5. Default realm locale - if(realm.getDefaultLocale() != null){ - return Locale.forLanguageTag(realm.getDefaultLocale()); - } - - return Locale.ENGLISH; - } - - public static void updateLocaleCookie(Response.ResponseBuilder builder, - Locale locale, - RealmModel realm, - UriInfo uriInfo, - String path) { - if (locale == null) { - return; - } - boolean secure = realm.getSslRequired().isRequired(uriInfo.getRequestUri().getHost()); - builder.cookie(new NewCookie(LocaleHelper.LOCALE_COOKIE, locale.toLanguageTag(), path, null, null, 31536000, secure)); - } - - public static Locale findLocale(Set supportedLocales, String ... localeStrings) { - for(String localeString : localeStrings){ - Locale result = null; - Locale search = Locale.forLanguageTag(localeString); - for(String languageTag : supportedLocales) { - Locale locale = Locale.forLanguageTag(languageTag); - if(locale.getLanguage().equals(search.getLanguage())){ - if(locale.getCountry().equals("") && result == null){ - result = locale; - } - if(locale.getCountry().equals(search.getCountry())){ - return locale; - } - } - } - if(result != null){ - return result; - } - } - return null; - } -} diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/LocaleBean.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/LocaleBean.java index 91ae108147..aedf33e3c4 100644 --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/LocaleBean.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/LocaleBean.java @@ -1,6 +1,5 @@ package org.keycloak.freemarker.beans; -import org.keycloak.freemarker.LocaleHelper; import org.keycloak.models.RealmModel; import javax.ws.rs.core.UriBuilder; @@ -22,7 +21,7 @@ public class LocaleBean { supported = new LinkedList<>(); for (String l : realm.getSupportedLocales()) { String label = messages.getProperty("locale_" + l, l); - String url = uriBuilder.replaceQueryParam(LocaleHelper.KC_LOCALE_PARAM, l).build().toString(); + String url = uriBuilder.replaceQueryParam("kc_locale", l).build().toString(); supported.add(new Locale(label, url)); } } diff --git a/forms/common-freemarker/src/test/java/org/keycloak/freemarke/LocaleHelperTest.java b/forms/common-freemarker/src/test/java/org/keycloak/freemarke/LocaleHelperTest.java deleted file mode 100644 index 47fa9e4cb8..0000000000 --- a/forms/common-freemarker/src/test/java/org/keycloak/freemarke/LocaleHelperTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.keycloak.freemarke; - -import org.junit.Assert; -import org.junit.Test; -import org.keycloak.freemarker.LocaleHelper; - -import java.util.Arrays; -import java.util.HashSet; - -/** - * @author Michael Gerber - */ -public class LocaleHelperTest { - @Test - public void findLocaleTest(){ - Assert.assertEquals("de", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","en")), "de").toLanguageTag()); - Assert.assertEquals("en", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","en")), "en").toLanguageTag()); - Assert.assertEquals("de", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","en")), "de-CH").toLanguageTag()); - Assert.assertEquals("de-CH", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","de-CH","de-DE")), "de-CH").toLanguageTag()); - Assert.assertEquals("de-DE", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","de-CH","de-DE")), "de-DE").toLanguageTag()); - Assert.assertEquals("de", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","de-CH","de-DE")), "de").toLanguageTag()); - Assert.assertNull(LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de-CH","de-DE")), "de")); - } - - @Test - public void findLocalesTest(){ - Assert.assertEquals("de", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","en")), "de en".split(" ")).toLanguageTag()); - Assert.assertEquals("en", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","en")), "en de".split(" ")).toLanguageTag()); - Assert.assertEquals("de", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","en")), "de-CH en".split(" ")).toLanguageTag()); - Assert.assertEquals("de-CH", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","de-CH","de-DE")), "de-CH de".split(" ")).toLanguageTag()); - Assert.assertEquals("de-DE", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","de-CH","de-DE")), "de-DE de-CH de".split(" ")).toLanguageTag()); - Assert.assertEquals("de", LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de","de-CH","de-DE")), "en fr de".split(" ")).toLanguageTag()); - Assert.assertNull(LocaleHelper.findLocale(new HashSet<>(Arrays.asList("de-CH","de-DE")), "de en fr".split(" "))); - } -} diff --git a/forms/email-freemarker/pom.xml b/forms/email-freemarker/pom.xml index e1eff9db3d..0b9f540016 100755 --- a/forms/email-freemarker/pom.xml +++ b/forms/email-freemarker/pom.xml @@ -29,6 +29,11 @@ keycloak-model-api provided + + org.keycloak + keycloak-services + provided + org.keycloak keycloak-events-api diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java index b620a5968b..f049085452 100755 --- a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java +++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java @@ -24,7 +24,6 @@ import org.keycloak.events.Event; import org.keycloak.events.EventType; import org.keycloak.freemarker.FreeMarkerException; import org.keycloak.freemarker.FreeMarkerUtil; -import org.keycloak.freemarker.LocaleHelper; import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.ThemeProvider; import org.keycloak.freemarker.beans.MessageFormatterMethod; @@ -110,7 +109,7 @@ public class FreeMarkerEmailProvider implements EmailProvider { try { ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL); - Locale locale = LocaleHelper.getLocale(realm, user); + Locale locale = session.getContext().resolveLocale(user); attributes.put("locale", locale); Properties rb = theme.getMessages(locale); attributes.put("msg", new MessageFormatterMethod(locale, rb)); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index 1a16fbcd26..7fc1bcddcb 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -24,7 +24,6 @@ import org.keycloak.email.EmailProvider; import org.keycloak.freemarker.BrowserSecurityHeaderSetup; import org.keycloak.freemarker.FreeMarkerException; import org.keycloak.freemarker.FreeMarkerUtil; -import org.keycloak.freemarker.LocaleHelper; import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.ThemeProvider; import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod; @@ -201,7 +200,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } Properties messagesBundle; - Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, session.getContext().getRequestHeaders()); + Locale locale = session.getContext().resolveLocale(user); try { messagesBundle = theme.getMessages(locale); attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle)); @@ -292,10 +291,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { for (Map.Entry entry : httpResponseHeaders.entrySet()) { builder.header(entry.getKey(), entry.getValue()); } - - String cookiePath = Urls.localeCookiePath(baseUri, realm.getName()); - LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, cookiePath); - return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); @@ -345,7 +340,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } Properties messagesBundle; - Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, session.getContext().getRequestHeaders()); + Locale locale = session.getContext().resolveLocale(user); try { messagesBundle = theme.getMessages(locale); attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle)); @@ -393,9 +388,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { for (Map.Entry entry : httpResponseHeaders.entrySet()) { builder.header(entry.getKey(), entry.getValue()); } - - String cookiePath = Urls.localeCookiePath(baseUri, realm.getName()); - LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, cookiePath); return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java index 760ffe4624..e66927af63 100755 --- a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java +++ b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java @@ -5,6 +5,7 @@ import org.keycloak.models.utils.RealmImporter; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.UriInfo; +import java.util.Locale; /** * @author Stian Thorgersen @@ -33,4 +34,6 @@ public interface KeycloakContext { RealmImporter getRealmManager(); + Locale resolveLocale(UserModel user); + } diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index f57bb04199..9dec77844c 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -1,6 +1,5 @@ package org.keycloak.models; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -10,11 +9,11 @@ import java.util.Set; * @version $Revision: 1 $ */ public interface UserModel { - public static final String USERNAME = "username"; - public static final String LAST_NAME = "lastName"; - public static final String FIRST_NAME = "firstName"; - public static final String EMAIL = "email"; - public static final String LOCALE = "locale"; + String USERNAME = "username"; + String LAST_NAME = "lastName"; + String FIRST_NAME = "firstName"; + String EMAIL = "email"; + String LOCALE = "locale"; String getId(); diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java index 665662dcee..ec866eb2df 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java @@ -2,16 +2,15 @@ package org.keycloak.services; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.ClientConnection; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakContext; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; +import org.keycloak.models.*; import org.keycloak.models.utils.RealmImporter; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakApplication; +import org.keycloak.services.util.LocaleHelper; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.UriInfo; +import java.util.Locale; /** * @author Stian Thorgersen @@ -87,4 +86,9 @@ public class DefaultKeycloakContext implements KeycloakContext { manager.setContextPath(getContextPath()); return manager; } + + @Override + public Locale resolveLocale(UserModel user) { + return LocaleHelper.getLocale(session, realm, user); + } } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 73d274edc9..332d71d136 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -33,42 +33,24 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.login.LoginFormsProvider; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.RequiredActionProviderModel; -import org.keycloak.models.UserConsentModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; +import org.keycloak.models.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; +import org.keycloak.services.Urls; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.services.resources.RealmsResource; -import org.keycloak.services.Urls; import org.keycloak.services.util.CookieHelper; import org.keycloak.util.Time; -import javax.ws.rs.core.Cookie; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.NewCookie; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; - +import javax.ws.rs.core.*; import java.net.URI; import java.util.LinkedList; import java.util.List; -import java.util.Locale; import java.util.Set; -import org.keycloak.freemarker.LocaleHelper; -import org.keycloak.util.TokenUtil; /** * Stateless object that manages authentication @@ -413,7 +395,8 @@ public class AuthenticationManager { } } - handleLoginLocale(realm, userSession, request, uriInfo); + // Updates users locale if required + session.getContext().resolveLocale(userSession.getUser()); // refresh the cookies! createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection); @@ -428,17 +411,6 @@ public class AuthenticationManager { } - // If a locale has been set on the login screen, associate that locale with the user - private static void handleLoginLocale(RealmModel realm, UserSessionModel userSession, - HttpRequest request, UriInfo uriInfo) { - Cookie localeCookie = request.getHttpHeaders().getCookies().get(LocaleHelper.LOCALE_COOKIE); - if (localeCookie == null) return; - - UserModel user = userSession.getUser(); - Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, request.getHttpHeaders()); - user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag()); - } - public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) { diff --git a/services/src/main/java/org/keycloak/services/util/LocaleHelper.java b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java new file mode 100644 index 0000000000..1f523d7132 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.keycloak.services.util; + +import org.jboss.resteasy.spi.HttpResponse; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.util.ServerCookie; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriInfo; +import java.util.Locale; +import java.util.Set; + +/** + * @author Michael Gerber + */ +public class LocaleHelper { + + private static final String LOCALE_COOKIE = "KEYCLOAK_LOCALE"; + private static final String UI_LOCALES_PARAM = "ui_locales"; + private static final String KC_LOCALE_PARAM = "kc_locale"; + + public static Locale getLocale(KeycloakSession session, RealmModel realm, UserModel user) { + if (!realm.isInternationalizationEnabled()) { + return Locale.ENGLISH; + } else { + Locale locale = getUserLocale(session, realm, user); + return locale != null ? locale : Locale.forLanguageTag(realm.getDefaultLocale()); + } + } + + private static Locale getUserLocale(KeycloakSession session, RealmModel realm, UserModel user) { + UriInfo uriInfo = session.getContext().getUri(); + HttpHeaders httpHeaders = session.getContext().getRequestHeaders(); + + // kc_locale query parameter + if (uriInfo != null && uriInfo.getQueryParameters().containsKey(KC_LOCALE_PARAM)) { + String localeString = uriInfo.getQueryParameters().getFirst(KC_LOCALE_PARAM); + Locale locale = findLocale(realm.getSupportedLocales(), localeString); + if (locale != null) { + updateLocaleCookie(session, realm, localeString); + if (user != null) { + updateUsersLocale(user, localeString); + } + return locale; + } + } + + // User profile + if (user != null && user.getAttributes().containsKey(UserModel.LOCALE)) { + String localeString = user.getFirstAttribute(UserModel.LOCALE); + Locale locale = findLocale(realm.getSupportedLocales(), localeString); + if (locale != null) { + updateLocaleCookie(session, realm, localeString); + return locale; + } + } + + // Locale cookie + if (httpHeaders != null && httpHeaders.getCookies().containsKey(LOCALE_COOKIE)) { + String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue(); + Locale locale = findLocale(realm.getSupportedLocales(), localeString); + if (locale != null) { + if (user != null) { + updateUsersLocale(user, localeString); + } + return locale; + } + } + + // ui_locales query parameter + if (uriInfo != null && uriInfo.getQueryParameters().containsKey(UI_LOCALES_PARAM)) { + String localeString = uriInfo.getQueryParameters().getFirst(UI_LOCALES_PARAM); + Locale locale = findLocale(realm.getSupportedLocales(), localeString.split(" ")); + if (locale != null) { + return locale; + } + } + + // Accept-Language http header + if (httpHeaders != null && httpHeaders.getAcceptableLanguages() != null && !httpHeaders.getAcceptableLanguages().isEmpty()) { + for (Locale l : httpHeaders.getAcceptableLanguages()) { + String localeString = l.toLanguageTag(); + Locale locale = findLocale(realm.getSupportedLocales(), localeString); + if (locale != null) { + return locale; + } + } + } + + return null; + } + + private static void updateLocaleCookie(KeycloakSession session, + RealmModel realm, + String locale) { + boolean secure = realm.getSslRequired().isRequired(session.getContext().getUri().getRequestUri().getHost()); + addCookie(LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, session.getContext().getUri()), null, null, -1, secure, true); + } + + private static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) { + HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class); + StringBuffer cookieBuf = new StringBuffer(); + ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly); + String cookie = cookieBuf.toString(); + response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie); + } + + private static Locale findLocale(Set supportedLocales, String... localeStrings) { + for (String localeString : localeStrings) { + Locale result = null; + Locale search = Locale.forLanguageTag(localeString); + for (String languageTag : supportedLocales) { + Locale locale = Locale.forLanguageTag(languageTag); + if (locale.getLanguage().equals(search.getLanguage())) { + if (locale.getCountry().equals("") && result == null) { + result = locale; + } + if (locale.getCountry().equals(search.getCountry())) { + return locale; + } + } + } + if (result != null) { + return result; + } + } + return null; + } + + private static void updateUsersLocale(UserModel user, String locale) { + if (!locale.equals(user.getFirstAttribute("locale"))) { + user.setSingleAttribute(UserModel.LOCALE, locale); + } + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index 087f82faec..2a08ca12a6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -24,7 +24,6 @@ package org.keycloak.testsuite; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URLEncodedUtils; @@ -37,7 +36,6 @@ import org.keycloak.OAuth2Constants; import org.keycloak.RSATokenVerifier; import org.keycloak.VerificationException; import org.keycloak.constants.AdapterConstants; -import org.keycloak.freemarker.LocaleHelper; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; @@ -397,7 +395,7 @@ public class OAuthClient { b.queryParam(OAuth2Constants.STATE, state); } if(uiLocales != null){ - b.queryParam(LocaleHelper.UI_LOCALES_PARAM, uiLocales); + b.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales); } if (scope != null) { b.queryParam(OAuth2Constants.SCOPE, scope); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java index 31e11a4b97..c0ca3e81c9 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java @@ -54,7 +54,6 @@ public class EmailTest { UserModel user = manager.getSession().users().addUser(appRealm, "login-test"); user.setEmail("login@test.com"); user.setEnabled(true); - user.setSingleAttribute(UserModel.LOCALE, "de"); UserCredentialModel creds = new UserCredentialModel(); creds.setType(CredentialRepresentation.PASSWORD); @@ -86,7 +85,7 @@ public class EmailTest { MimeMessage message = greenMail.getReceivedMessages()[0]; - Assert.assertEquals("Passwort zurückzusetzen", message.getSubject()); + Assert.assertEquals("Reset password", message.getSubject()); keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -106,4 +105,31 @@ public class EmailTest { Assert.assertEquals("Reset password", message.getSubject()); } + + @Test + public void restPasswordEmailGerman() throws IOException, MessagingException { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + manager.getSession().users().getUserByUsername("login-test", appRealm).setSingleAttribute(UserModel.LOCALE, "de"); + } + }); + + loginPage.open(); + loginPage.resetPassword(); + resetPasswordPage.changePassword("login-test"); + + assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + Assert.assertEquals("Passwort zurückzusetzen", message.getSubject()); + + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + manager.getSession().users().getUserByUsername("login-test", appRealm).setSingleAttribute(UserModel.LOCALE, "en"); + } + }); + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java index 2d0f9eb11f..c05dac35ac 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java @@ -30,7 +30,6 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.adapters.HttpClientBuilder; -import org.keycloak.freemarker.LocaleHelper; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; @@ -87,7 +86,7 @@ public class LoginPageTest { //test if cookie works oauth.uiLocales("de"); loginPage.open(); - Assert.assertEquals("English", loginPage.getLanguageDropdownText()); + Assert.assertEquals("Deutsch", loginPage.getLanguageDropdownText()); driver.manage().deleteAllCookies(); loginPage.open(); @@ -113,10 +112,8 @@ public class LoginPageTest { loginPage.open(); Response response = client.target(driver.getCurrentUrl()).request().acceptLanguage("de").get(); Assert.assertTrue(response.readEntity(String.class).contains("Anmeldung bei test")); - Assert.assertEquals("de", response.getCookies().get(LocaleHelper.LOCALE_COOKIE).getValue()); response = client.target(driver.getCurrentUrl()).request().acceptLanguage("en").get(); Assert.assertTrue(response.readEntity(String.class).contains("Log in to test")); - Assert.assertEquals("en", response.getCookies().get(LocaleHelper.LOCALE_COOKIE).getValue()); } }