diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml index a723490bb6..b545aa3051 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml @@ -73,6 +73,12 @@ + + + + + + @@ -88,11 +94,14 @@ + + + diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index 023172f9ae..532807e53d 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -1,10 +1,6 @@ package org.keycloak.representations.idm; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * @author Bill Burke @@ -68,6 +64,10 @@ public class RealmRepresentation { private List identityProviders; private List protocolMappers; private Boolean identityFederationEnabled; + protected Boolean internationalizationEnabled; + protected Set supportedLocales; + protected String defaultLocale; + public String getId() { return id; @@ -513,4 +513,31 @@ public class RealmRepresentation { public void setProtocolMappers(List protocolMappers) { this.protocolMappers = protocolMappers; } + + public Boolean isInternationalizationEnabled() { + return internationalizationEnabled; + } + + public void setInternationalizationEnabled(Boolean internationalizationEnabled) { + this.internationalizationEnabled = internationalizationEnabled; + } + + public Set getSupportedLocales() { + if(supportedLocales == null){ + supportedLocales = new HashSet(); + } + return supportedLocales; + } + + public void setSupportedLocales(Set supportedLocales) { + this.supportedLocales = supportedLocales; + } + + public String getDefaultLocale() { + return defaultLocale; + } + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } } diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java b/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java index 7677a2091e..bc6920b5fa 100755 --- a/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java +++ b/forms/account-api/src/main/java/org/keycloak/account/AccountProvider.java @@ -1,11 +1,13 @@ package org.keycloak.account; +import org.apache.http.client.methods.HttpHead; import org.keycloak.events.Event; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.provider.Provider; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -18,13 +20,15 @@ public interface AccountProvider extends Provider { AccountProvider setUriInfo(UriInfo uriInfo); + AccountProvider setHttpHeaders(HttpHeaders httpHeaders); + Response createResponse(AccountPages page); - AccountProvider setError(String message); + AccountProvider setError(String message, Object ... parameters); - AccountProvider setSuccess(String message); + AccountProvider setSuccess(String message, Object ... parameters); - AccountProvider setWarning(String message); + AccountProvider setWarning(String message, Object ... parameters); AccountProvider setUser(UserModel user); 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 368b4591e3..881606f47f 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 @@ -3,38 +3,21 @@ package org.keycloak.account.freemarker; import org.jboss.logging.Logger; import org.keycloak.account.AccountPages; import org.keycloak.account.AccountProvider; -import org.keycloak.account.freemarker.model.AccountBean; -import org.keycloak.account.freemarker.model.AccountFederatedIdentityBean; -import org.keycloak.account.freemarker.model.FeaturesBean; -import org.keycloak.account.freemarker.model.LogBean; -import org.keycloak.account.freemarker.model.MessageBean; -import org.keycloak.account.freemarker.model.PasswordBean; -import org.keycloak.account.freemarker.model.ReferrerBean; -import org.keycloak.account.freemarker.model.SessionsBean; -import org.keycloak.account.freemarker.model.TotpBean; -import org.keycloak.account.freemarker.model.UrlBean; +import org.keycloak.account.freemarker.model.*; import org.keycloak.events.Event; -import org.keycloak.freemarker.BrowserSecurityHeaderSetup; -import org.keycloak.freemarker.FreeMarkerException; -import org.keycloak.freemarker.FreeMarkerUtil; -import org.keycloak.freemarker.Theme; -import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.freemarker.*; +import org.keycloak.freemarker.beans.TextFormatterBean; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.services.resources.flows.Urls; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; +import javax.ws.rs.core.*; import java.io.IOException; import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.text.MessageFormat; +import java.util.*; /** * @author Stian Thorgersen @@ -57,12 +40,14 @@ public class FreeMarkerAccountProvider implements AccountProvider { private boolean passwordSet; private KeycloakSession session; private FreeMarkerUtil freeMarker; + private HttpHeaders headers; public static enum MessageType {SUCCESS, WARNING, ERROR} private UriInfo uriInfo; private String message; + private Object[] parameters; private MessageType messageType; public FreeMarkerAccountProvider(KeycloakSession session, FreeMarkerUtil freeMarker) { @@ -75,6 +60,12 @@ public class FreeMarkerAccountProvider implements AccountProvider { return this; } + @Override + public AccountProvider setHttpHeaders(HttpHeaders httpHeaders) { + this.headers = httpHeaders; + return this; + } + @Override public Response createResponse(AccountPages page) { Map attributes = new HashMap(); @@ -94,9 +85,14 @@ public class FreeMarkerAccountProvider implements AccountProvider { logger.warn("Failed to load properties", e); } + Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, headers); + if(locale != null){ + attributes.put("locale", locale); + attributes.put("formatter", new TextFormatterBean(locale)); + } Properties messages; try { - messages = theme.getMessages(); + messages = theme.getMessages(locale); attributes.put("rb", messages); } catch (IOException e) { logger.warn("Failed to load messages", e); @@ -115,13 +111,23 @@ public class FreeMarkerAccountProvider implements AccountProvider { } if (message != null) { - attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType)); + String formattedMessage; + if(messages.containsKey(message)){ + formattedMessage = new MessageFormat(messages.getProperty(message).replace("'","''"),locale).format(parameters); + }else{ + formattedMessage = message; + } + attributes.put("message", new MessageBean(formattedMessage, messageType)); } if (referrer != null) { attributes.put("referrer", new ReferrerBean(referrer)); } + if(realm != null){ + attributes.put("realm", new RealmBean(realm)); + } + attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri(), stateChecker)); attributes.put("features", new FeaturesBean(identityProviderEnabled, eventsEnabled, passwordUpdateSupported)); @@ -150,6 +156,7 @@ 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); + LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri,realm.getName())); return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); @@ -163,22 +170,25 @@ public class FreeMarkerAccountProvider implements AccountProvider { } @Override - public AccountProvider setError(String message) { + public AccountProvider setError(String message, Object ... parameters) { this.message = message; + this.parameters = parameters; this.messageType = MessageType.ERROR; return this; } @Override - public AccountProvider setSuccess(String message) { + public AccountProvider setSuccess(String message, Object ... parameters) { this.message = message; + this.parameters = parameters; this.messageType = MessageType.SUCCESS; return this; } @Override - public AccountProvider setWarning(String message) { + public AccountProvider setWarning(String message, Object ... parameters) { this.message = message; + this.parameters = parameters; this.messageType = MessageType.WARNING; return this; } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/RealmBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/RealmBean.java new file mode 100755 index 0000000000..b0a5eb4959 --- /dev/null +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/RealmBean.java @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.account.freemarker.model; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.representations.idm.CredentialRepresentation; + +import java.util.Set; + +/** + * @author Michael Gerber + */ +public class RealmBean { + + private RealmModel realm; + + public RealmBean(RealmModel realmModel) { + realm = realmModel; + } + + public boolean isInternationalizationEnabled() { + return realm.isInternationalizationEnabled(); + } + + public Set getSupportedLocales(){ + return realm.getSupportedLocales(); + } + +} diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java index de7e432d6a..a357aadd78 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/UrlBean.java @@ -59,6 +59,10 @@ public class UrlBean { return Urls.accountSessionsLogoutPage(baseQueryURI, realm, stateChecker).toString(); } + public String getLocaleCookiePath(){ + return Urls.localeCookiePath(baseURI, realm); + } + public String getTotpRemoveUrl() { return Urls.accountTotpRemove(baseQueryURI, realm, stateChecker).toString(); } diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java index 533b63120d..1fefce1921 100644 --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java @@ -7,14 +7,7 @@ import org.keycloak.models.KeycloakSession; import java.io.IOException; import java.io.InputStream; import java.net.URL; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.ListIterator; -import java.util.Properties; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -228,11 +221,11 @@ public class ExtendingThemeManager implements ThemeProvider { } @Override - public Properties getMessages() throws IOException { + public Properties getMessages(Locale locale) throws IOException { Properties messages = new Properties(); ListIterator itr = themes.listIterator(themes.size()); while (itr.hasPrevious()) { - Properties m = itr.previous().getMessages(); + Properties m = itr.previous().getMessages(locale); if (m != null) { messages.putAll(m); } 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 new file mode 100644 index 0000000000..e153fcf511 --- /dev/null +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/LocaleHelper.java @@ -0,0 +1,113 @@ +package org.keycloak.freemarker; + +import org.jboss.logging.Logger; +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 LOCALE_PARAM = "ui_locale"; + + private final static Logger LOGGER = Logger.getLogger(LocaleHelper.class); + + 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; + } + + //1. Locale cookie + if(httpHeaders != null && httpHeaders.getCookies().containsKey(LOCALE_COOKIE)){ + String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue(); + Locale locale = findLocale(localeString, realm.getSupportedLocales()); + if(locale != null){ + if(user != null){ + user.setAttribute(UserModel.LOCALE, locale.toLanguageTag()); + } + return locale; + }else{ + LOGGER.infof("Locale %s is not supported.", localeString); + } + } + + //2. User profile + if(user != null && user.getAttributes().containsKey(UserModel.LOCALE)){ + String localeString = user.getAttribute(UserModel.LOCALE); + Locale locale = findLocale(localeString, realm.getSupportedLocales()); + if(locale != null){ + + return locale; + }else{ + LOGGER.infof("Locale %s is not supported.", localeString); + } + } + + //3. ui_locales query parameter + if(uriInfo != null && uriInfo.getQueryParameters().containsKey(LOCALE_PARAM)){ + String localeString = uriInfo.getQueryParameters().getFirst(LOCALE_PARAM); + Locale locale = findLocale(localeString, realm.getSupportedLocales()); + if(locale != null){ + return locale; + }else{ + LOGGER.infof("Locale %s is not supported.", localeString); + } + } + + //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(localeString, realm.getSupportedLocales()); + if(locale != null){ + return locale; + }else{ + LOGGER.infof("Locale %s is not supported.", localeString); + } + } + } + + //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(String localeString, Set supportedLocales) { + 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; + } + } + } + return result; + } +} diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java index dff92ee861..6a12a49f00 100644 --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java @@ -3,6 +3,7 @@ package org.keycloak.freemarker; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.Locale; import java.util.Properties; /** @@ -28,7 +29,7 @@ public interface Theme { public InputStream getResourceAsStream(String path) throws IOException; - public Properties getMessages() throws IOException; + public Properties getMessages(Locale locale) throws IOException; public Properties getProperties() throws IOException; diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/TextFormatterBean.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/TextFormatterBean.java new file mode 100644 index 0000000000..5243d56cae --- /dev/null +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/TextFormatterBean.java @@ -0,0 +1,19 @@ +package org.keycloak.freemarker.beans; + +import java.text.MessageFormat; +import java.util.Locale; + +/** + * @author Michael Gerber + */ +public class TextFormatterBean { + private Locale locale; + + public TextFormatterBean(Locale locale) { + this.locale = locale; + } + + public String format(String pattern, Object ... parameters){ + return new MessageFormat(pattern.replace("'","''"),locale).format(parameters); + } +} 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 new file mode 100644 index 0000000000..01bb9986b2 --- /dev/null +++ b/forms/common-freemarker/src/test/java/org/keycloak/freemarke/LocaleHelperTest.java @@ -0,0 +1,24 @@ +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("de", new HashSet<>(Arrays.asList("de","en"))).toLanguageTag()); + Assert.assertEquals("en", LocaleHelper.findLocale("en", new HashSet<>(Arrays.asList("de","en"))).toLanguageTag()); + Assert.assertEquals("de", LocaleHelper.findLocale("de-CH", new HashSet<>(Arrays.asList("de","en"))).toLanguageTag()); + Assert.assertEquals("de-CH", LocaleHelper.findLocale("de-CH", new HashSet<>(Arrays.asList("de","de-CH","de-DE"))).toLanguageTag()); + Assert.assertEquals("de-DE", LocaleHelper.findLocale("de-DE", new HashSet<>(Arrays.asList("de","de-CH","de-DE"))).toLanguageTag()); + Assert.assertEquals("de", LocaleHelper.findLocale("de", new HashSet<>(Arrays.asList("de","de-CH","de-DE"))).toLanguageTag()); + Assert.assertNull(LocaleHelper.findLocale("de", new HashSet<>(Arrays.asList("de-CH","de-DE")))); + } +} diff --git a/forms/common-themes/pom.xml b/forms/common-themes/pom.xml index 8ad524fd80..04ea6bc30e 100755 --- a/forms/common-themes/pom.xml +++ b/forms/common-themes/pom.xml @@ -42,6 +42,11 @@ resteasy-jaxrs provided + + org.jboss.logging + jboss-logging + provided + diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java b/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java index 1a793eee7c..c95edd2ea7 100755 --- a/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java +++ b/forms/common-themes/src/main/java/org/keycloak/theme/ClassLoaderTheme.java @@ -2,10 +2,13 @@ package org.keycloak.theme; import org.keycloak.freemarker.Theme; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.Locale; import java.util.Properties; +import java.util.ResourceBundle; /** * @author Stian Thorgersen @@ -26,7 +29,7 @@ public class ClassLoaderTheme implements Theme { private String resourceRoot; - private String messages; + private String messageRoot; private Properties properties; @@ -43,7 +46,7 @@ public class ClassLoaderTheme implements Theme { this.templateRoot = themeRoot; this.resourceRoot = themeRoot + "resources/"; - this.messages = themeRoot + "messages/messages.properties"; + this.messageRoot = themeRoot + "messages/"; this.properties = new Properties(); URL p = classLoader.getResource(themeRoot + "theme.properties"); @@ -102,9 +105,13 @@ public class ClassLoaderTheme implements Theme { } @Override - public Properties getMessages() throws IOException { + public Properties getMessages(Locale locale) throws IOException { + if(locale == null){ + return null; + } Properties m = new Properties(); - URL url = classLoader.getResource(this.messages); + + URL url = classLoader.getResource(this.messageRoot + "messages_" + locale.toString() + ".properties"); if (url != null) { m.load(url.openStream()); } diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java index aa2312e707..0edc92ee5f 100644 --- a/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java +++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderTheme.java @@ -7,6 +7,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.util.Locale; import java.util.Properties; /** @@ -81,9 +82,14 @@ public class FolderTheme implements Theme { } @Override - public Properties getMessages() throws IOException { + public Properties getMessages(Locale locale) throws IOException { + if(locale == null){ + return null; + } + Properties m = new Properties(); - File file = new File(themeDir, "messages" + File.separator + "messages.properties"); + + File file = new File(themeDir, "messages" + File.separator + "messages_" + locale.toString() + ".properties"); if (file.isFile()) { m.load(new FileInputStream(file)); } diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties deleted file mode 100755 index 9ada6842b3..0000000000 --- a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties +++ /dev/null @@ -1,48 +0,0 @@ -authenticatorCode=One-time code -email=Email -errorHeader=Error! -firstName=First name -lastName=Last name -password=Password -passwordConfirm=Confirmation -passwordNew=New Password -successHeader=Success! -username=Username -street=Street -locality=City or Locality -region=State, Province, or Region -postal_code=Zip or Postal code -country=Country - -missingFirstName=Please specify first name -invalidEmail=Invalid email address -missingLastName=Please specify last name -missingEmail=Please specify email -missingPassword=Please specify password. -notMatchPassword=Passwords don't match - -missingTotp=Please specify authenticator code -invalidPasswordExisting=Invalid existing password -invalidPasswordConfirm=Password confirmation doesn't match -invalidTotp=Invalid authenticator code -readOnlyUser=You can't update your account as it is read only -readOnlyPassword=You can't update your password as your account is read only - -successTotp=Mobile authenticator configured. -successTotpRemoved=Mobile authenticator removed. - -accountUpdated=Your account has been updated -accountPasswordUpdated=Your password has been updated - -missingIdentityProvider=Identity provider not specified -invalidFederatedIdentityAction=Invalid or missing action -identityProviderNotFound=Specified identity provider not found -federatedIdentityLinkNotActive=This identity is not active anymore -federatedIdentityRemovingLastProvider=You can't remove last federated identity as you don't have password -identityProviderRedirectError=Failed to redirect to identity provider -identityProviderRemoved=Identity provider removed successfully - -accountDisabled=Account is disabled, contact admin\ -accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later - -logOutAllSessions=Log out all sessions diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages_de.properties new file mode 100644 index 0000000000..4d3e4b37bc --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/base/messages/messages_de.properties @@ -0,0 +1,57 @@ +authenticatorCode=One-time code +email=E-Mail +firstName=Vorname +lastName=Nachname +password=Passwort +passwordConfirm=Passwort bestätigung +passwordNew=Neues Passwort +username=Benutzernamen +street=Strasse +region=Staat, Provinz, Region +postal_code=PLZ +locality=Stadt oder Ortschaft +country=Land + +missingFirstNameMessage=Bitte geben Sie einen Vornamen ein. +missingEmailMessage=Bitte geben Sie eine E-Mail Adresse ein. +missingLastNameMessage=Bitte geben Sie einen Nachnamen ein. +missingPasswordMessage=Bitte geben Sie ein Passwort ein. +notMatchPasswordMessage=Passwörter sind nicht identisch. + +missingTotpMessage=Bitte geben Sie den One-time Code ein. +invalidPasswordExistingMessage=Das aktuelle Passwort is ungültig. +invalidPasswordConfirmMessage=Die Passwort bestätigung ist nicht identisch. +invalidTotpMessage=Ungültiger One-time Code. +invalidEmailMessage=Ungültige E-Mail Adresse. + +readOnlyUserMessage=Sie können dieses Benutzerkonto nicht ändern, da es schreibgeschützt ist. +readOnlyPasswordMessage=Sie können dieses Passwort nicht ändern, da es schreibgeschützt ist. + +successTotpMessage=Mobile Authentifizierung eingerichtet. +successTotpRemovedMessage=Mobile Authentifizierung entfernt. + +accountUpdatedMessage=Ihr Benutzerkonto wurde aktualisiert. +accountPasswordUpdatedMessage=Ihr Passwort wurde aktualisiert. + +missingIdentityProviderMessage=Identity Provider nicht angegeben. +invalidFederatedIdentityActionMessage=Ungültige oder fehlende Aktion. +identityProviderNotFoundMessage=Angegebener Identity Provider nicht gefunden. +federatedIdentityLinkNotActiveMessage=Diese Identität ist nicht mehr aktiv. +federatedIdentityRemovingLastProviderMessage=Sie können den letzen Eintrag nicht enfernen, da Sie kein Passwort haben. +identityProviderRedirectErrorMessage=Fehler bei der Weiterleitung zum Identity Proivder. +identityProviderRemovedMessage=Identity Provider erfolgreich entfernt. + +accountDisabledMessage=Benutzerkonto ist gesperrt, bitte kontaktieren Sie den Admin. + +doLogOutAllSessions=Alle Sessionen abmelden + +accountTemporarilyDisabledMessage=Benutzerkonto ist temporär gespert, bitte kontaktieren Sie den Admin oder versuchen Sie es später nocheinmal. +invalidPasswordMinLengthMessage=Ungültiges Passwort: minimum länge {0}. +invalidPasswordMinDigitsMessage=Ungültiges Passwort: muss mindestens {0} Zahl(en) beinhalten. +invalidPasswordMinLowerCaseCharsMessage=Ungültiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten. +invalidPasswordMinUpperCaseCharsMessage=Ungültiges Passwort: muss mindestens {0} Grossbuchstaben beinhalten. +invalidPasswordMinSpecialCharsMessage=Ungültiges Passwort\: muss mindestens {0} Spezialzeichen beinhalten. +invalidPasswordNotUsernameMessage=Ungültiges Passwort\: darf nicht gleich sein wie Benutzername. + +locale_de=Deutsch +locale_en=Englisch diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages_en.properties new file mode 100755 index 0000000000..41072c8fd8 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/account/base/messages/messages_en.properties @@ -0,0 +1,57 @@ +authenticatorCode=One-time code +email=Email +firstName=First name +lastName=Last name +password=Password +passwordConfirm=Confirmation +passwordNew=New Password +username=Username +street=Street +locality=City or Locality +region=State, Province, or Region +postal_code=Zip or Postal code +country=Country + +missingFirstNameMessage=Please specify first name. +invalidEmailMessage=Invalid email address. +missingLastNameMessage=Please specify last name. +missingEmailMessage=Please specify email. +missingPasswordMessage=Please specify password. +notMatchPasswordMessage=Passwords don't match. + +missingTotpMessage=Please specify authenticator code +invalidPasswordExistingMessage=Invalid existing password +invalidPasswordConfirmMessage=Password confirmation doesn't match +invalidTotpMessage=Invalid authenticator code + +readOnlyUserMessage=You can't update your account as it is read only +readOnlyPasswordMessage=You can't update your password as your account is read only + +successTotpMessage=Mobile authenticator configured. +successTotpRemovedMessage=Mobile authenticator removed. + +accountUpdatedMessage=Your account has been updated +accountPasswordUpdatedMessage=Your password has been updated + +missingIdentityProviderMessage=Identity provider not specified +invalidFederatedIdentityActionMessage=Invalid or missing action +identityProviderNotFoundMessage=Specified identity provider not found +federatedIdentityLinkNotActiveMessage=This identity is not active anymore +federatedIdentityRemovingLastProviderMessage=You can't remove last federated identity as you don't have password +identityProviderRedirectErrorMessage=Failed to redirect to identity provider +identityProviderRemovedMessage=Identity provider removed successfully + +accountDisabledMessage=Account is disabled, contact admin + +doLogOutAllSessions=Log out all sessions + +accountTemporarilyDisabledMessage=Account is temporarily disabled, contact admin or try again later +invalidPasswordMinLengthMessage=Invalid password: minimum length {0} +invalidPasswordMinLowerCaseCharsMessage=Invalid password: must contain at least {0} lower case characters +invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} numerical digits +invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters +invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters +invalidPasswordNotUsernameMessage=Invalid password\: must not be equal to the username + +locale_de=German +locale_en=English diff --git a/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl b/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl index 85c7a16702..435f188f98 100755 --- a/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl @@ -42,6 +42,6 @@ - ${rb.logOutAllSessions} + ${rb.doLogOutAllSessions} diff --git a/forms/common-themes/src/main/resources/theme/account/base/template.ftl b/forms/common-themes/src/main/resources/theme/account/base/template.ftl index 11dc877bf4..0d862c3835 100644 --- a/forms/common-themes/src/main/resources/theme/account/base/template.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/template.ftl @@ -15,6 +15,19 @@ + <#if realm.internationalizationEnabled> + + @@ -28,6 +41,15 @@ +
+ +
+ +
+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html index 59fa3b2e36..5007f12ac1 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html @@ -94,12 +94,24 @@
-
+
+ +
+
+ +
+
+
diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl index 38d150fd5d..301212fff9 100644 --- a/forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl @@ -1,5 +1 @@ -Someone has created a Keycloak account with this email address. If this was you, click the link below to verify your email address: -${link} -This link will expire within ${linkExpiration} minutes. - -If you didn't create this account, just ignore this message. \ No newline at end of file +${formatter.format(rb.emailVerificationBody,link, linkExpiration)} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/event-login_error.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/event-login_error.ftl index c1227aa882..cd9d247d9d 100644 --- a/forms/common-themes/src/main/resources/theme/email/keycloak/event-login_error.ftl +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/event-login_error.ftl @@ -1 +1 @@ -A failed login attempt was dettected to your account on ${event.date?datetime} from ${event.ipAddress}. If this was not you, please contact an admin. \ No newline at end of file +${formatter.format(rb.eventLoginErrorBody,event.date,event.ipAddress)} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/event-remove_totp.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/event-remove_totp.ftl index c62e174053..37ae2f73a3 100644 --- a/forms/common-themes/src/main/resources/theme/email/keycloak/event-remove_totp.ftl +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/event-remove_totp.ftl @@ -1 +1 @@ -TOTP was removed from your account on ${event.date?datetime} from ${event.ipAddress}. If this was not you, please contact an admin. \ No newline at end of file +${formatter.format(rb.eventRemoveTotpBody,event.date, event.ipAddress)} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_password.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_password.ftl index 696a6e6181..2c88214097 100644 --- a/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_password.ftl +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_password.ftl @@ -1 +1 @@ -Your password was changed on ${event.date?datetime} from ${event.ipAddress}. If this was not you, please contact an admin. \ No newline at end of file +${formatter.format(rb.eventUpdatePasswordBody,event.date, event.ipAddress)} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_totp.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_totp.ftl index 531ae662be..b34a898086 100644 --- a/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_totp.ftl +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/event-update_totp.ftl @@ -1 +1 @@ -TOTP was updated for your account on ${event.date?datetime} from ${event.ipAddress}. If this was not you, please contact an admin. \ No newline at end of file +${formatter.format(rb.eventUpdateTotpBody,event.date, event.ipAddress)} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages.properties b/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages.properties deleted file mode 100755 index 3139aca340..0000000000 --- a/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages.properties +++ /dev/null @@ -1,2 +0,0 @@ -emailVerificationSubject=Verify email -passwordResetSubject=Reset password \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages_de.properties new file mode 100644 index 0000000000..a5a85f9ecf --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages_de.properties @@ -0,0 +1,12 @@ +emailVerificationSubject=E-Mail verifizieren +passwordResetSubject=Passwort zurückzusetzen +passwordResetBody=Jemand hat angeforder Ihr Keycloak Passwort zurückzusetzen. Falls das Sie waren, dann klicken Sie auf den folgenden Link um das Passwort zurückzusetzen.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie das Passwort nicht zurücksetzen möchten, dann können Sie diese E-Mail ignorieren. +emailVerificationBody=Jemand hat ein Keycloak Konto mit dieser E-Mail Adresse erstellt. Fall das Sie waren, dann klicken Sie auf den Link um die E-Mail Adresse zu verifizieren.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie dieses Konto nicht erstellt haben, dann können sie diese Nachricht ignorieren. +eventLoginErrorSubject=Fehlgeschlagene Anmeldung +eventLoginErrorBody=Jemand hat um {0} von {1} versucht sich mit ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin. +eventRemoveTotpSubject=TOTP Entfernt +eventRemoveTotpBody=TOTP wurde von ihrem Konto am {0} von {1} entfernt. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin. +eventUpdatePasswordSubject=Passwort Aktualisiert +eventUpdatePasswordBody=Ihr Passwort wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin. +eventUpdateTotpSubject=TOTP Aktualisiert +eventUpdateTotpBody=TOTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin. diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages_en.properties new file mode 100755 index 0000000000..e64a00fb68 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages_en.properties @@ -0,0 +1,12 @@ +emailVerificationSubject=Verify email +emailVerificationBody=Someone has created a Keycloak account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you didn't create this account, just ignore this message. +passwordResetSubject=Reset password +passwordResetBody=Someone just requested to change your Keycloak account's password. If this was you, click on the link below to set a new password\n\n{0}\n\nThis link will expire within {1} minutes.\n\nIf you don't want to reset your password, just ignore this message and nothing will be changed. +eventLoginErrorSubject=Login error +eventLoginErrorBody=A failed login attempt was dettected to your account on {0} from {1}. If this was not you, please contact an admin. +eventRemoveTotpSubject=Remove TOTP +eventRemoveTotpBody=TOTP was removed from your account on {0} from {1}. If this was not you, please contact an admin. +eventUpdatePasswordSubject=Update password +eventUpdatePasswordBody=Your password was changed on {0} from {1}. If this was not you, please contact an admin. +eventUpdateTotpSubject=Update TOTP +eventUpdateTotpBody=TOTP was updated for your account on {0} from {1}. If this was not you, please contact an admin. diff --git a/forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl b/forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl index 5d277e5e2b..55f0138bef 100644 --- a/forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl @@ -1,5 +1 @@ -Someone just requested to change your Keycloak account's password. If this was you, click on the link below to set a new password: -${link} -This link will expire within ${linkExpiration} minutes. - -If you don't want to reset your password, just ignore this message and nothing will be changed. \ No newline at end of file +${formatter.format(rb.passwordResetBody,link, linkExpiration)} \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-config-totp.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-config-totp.ftl index d71536c965..c884ad4b75 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-config-totp.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-config-totp.ftl @@ -23,7 +23,7 @@
- +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl index c881e9c9be..797b62d7ce 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-oauth-grant.ftl @@ -4,7 +4,7 @@ <#if section = "title"> ${rb.oauthGrantTitle} <#elseif section = "header"> - Temporary access for ${(realm.name)!''} requested by ${(client.clientId)!''}. + ${formatter.format(rb.oauthGrantTitleHtml,(realm.name!''), (client.clientId!''))} <#elseif section = "form">

${rb.oauthGrantRequest}

@@ -55,8 +55,8 @@
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl index aca7d76ac8..9fe7a15ac8 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-reset-password.ftl @@ -1,9 +1,9 @@ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> - ${rb.emailForgotHeader} + ${rb.emailForgotTitle} <#elseif section = "header"> - ${rb.emailForgotHeader} + ${rb.emailForgotTitle} <#elseif section = "form">
@@ -23,7 +23,7 @@
- +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-totp.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-totp.ftl index 613e8f2593..8661fa0fa5 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-totp.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-totp.ftl @@ -1,9 +1,9 @@ <#import "template.ftl" as layout> <@layout.registrationLayout; section> <#if section = "title"> - ${rb.loginTitle} ${realm.name} + ${formatter.format(rb.loginTitle,realm.name)} <#elseif section = "header"> - ${rb.loginTitle} ${realm.name} + ${formatter.format(rb.loginTitleHtml,realm.name)} <#elseif section = "form">
@@ -27,8 +27,8 @@
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-update-password.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-update-password.ftl index 826ef31416..bbc69a27f4 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-update-password.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-update-password.ftl @@ -1,9 +1,9 @@ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> - ${rb.emailUpdateHeader} + ${rb.updatePasswordTitle} <#elseif section = "header"> - ${rb.emailUpdateHeader} + ${rb.updatePasswordTitle} <#elseif section = "form">
@@ -31,7 +31,7 @@
- +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-update-profile.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-update-profile.ftl index 812e29eb70..c3e1d9b303 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-update-profile.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-update-profile.ftl @@ -40,7 +40,7 @@
- +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/login-verify-email.ftl b/forms/common-themes/src/main/resources/theme/login/base/login-verify-email.ftl index 19e9bee15b..ac48f95c99 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login-verify-email.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login-verify-email.ftl @@ -6,10 +6,10 @@ ${rb.emailVerifyTitle} <#elseif section = "form">

- ${rb.emailVerifyInstr} + ${rb.emailVerifyInstruction1}

-

${rb.emailVerifyInstrQ} - ${rb.emailVerifyClick} ${rb.emailVerifyResend} +

+ ${rb.emailVerifyInstruction2} ${rb.doClickHere} ${rb.emailVerifyInstruction3}

\ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/base/login.ftl b/forms/common-themes/src/main/resources/theme/login/base/login.ftl index e46cf9518d..e514b9ee6c 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/login.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/login.ftl @@ -2,15 +2,15 @@ <@layout.registrationLayout displayInfo=social.displayInfo; section> <#if section = "title"> <#if client.application> - ${rb.loginTitle} ${realm.name} + ${formatter.format(rb.loginTitle,(realm.name!''))} <#elseif client.oauthClient> - ${realm.name} ${rb.loginOauthTitle} + ${formatter.format(rb.loginOauthTitle,(realm.name!''))} <#elseif section = "header"> <#if client.application> - ${rb.loginTitle} ${(realm.name)!''} + ${formatter.format(rb.loginTitleHtml,(realm.name!''))} <#elseif client.oauthClient> - Temporary access for ${(realm.name)!''} requested by ${(client.clientId)!''}. + ${formatter.format(rb.loginOauthTitleHtml,(realm.name!''), (client.clientId!''))} <#elseif section = "form"> <#if realm.password> @@ -41,24 +41,24 @@
<#if realm.resetPasswordAllowed> - ${rb.loginForgot} ${rb.password}? + ${rb.doForgotPassword}
- - + +
@@ -75,7 +75,7 @@ <#elseif section = "info" > <#if realm.password && realm.registrationAllowed>
- ${rb.noAccount} ${rb.register} + ${rb.noAccount} ${rb.doRegister}
diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties deleted file mode 100755 index 0a59a69c69..0000000000 --- a/forms/common-themes/src/main/resources/theme/login/base/messages/messages.properties +++ /dev/null @@ -1,126 +0,0 @@ -logIn=Log in -logInTo=Log in to -logInWith=Log in with -noAccount=New user? -register=Register -registerWith=Register with -allRequired=All fields are required -alreadyHaveAccount=Already have an account? -street=Street -locality=City or Locality -region=State, Province, or Region -postal_code=Zip or Postal code -country=Country - -poweredByKeycloak=Powered by Keycloak - -username=Username -usernameOrEmail=Username or email -fullName=Full name -firstName=First name -lastName=Last name -email=Email -password=Password -rememberMe=Remember me -passwordConfirm=Confirm password -passwordNew=New Password -passwordNewConfirm=New Password confirmation -passwordUpdated=Password updated -cancel=Cancel -accept=Accept -submit=Submit -yes=Yes -no=No - -authenticatorCode=One-time code -clientCertificate=Client Certificate - -invalidUser=Invalid username or password. -invalidPassword=Invalid username or password. -invalidEmail=Invalid email address -accountDisabled=Account is disabled, contact admin -accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later -expiredCode=Login timeout. Please login again - -missingFirstName=Please specify first name -missingLastName=Please specify last name -missingEmail=Please specify email -missingUsername=Please specify username -missingPassword=Please specify password. -notMatchPassword=Passwords don't match -missingTotp=Please specify authenticator code - -invalidPasswordExisting=Invalid existing password -invalidPasswordConfirm=Password confirmation doesn't match -invalidTotp=Invalid authenticator code - -successTotp=Mobile authenticator configured. -successTotpRemoved=Mobile authenticator removed. - -usernameExists=Username already exists -emailExists=Email already exists - -federatedIdentityEmailExists=User with email already exists. Please login to account management to link the account. -federatedIdentityUsernameExists=User with username already exists. Please login to account management to link the account. -federatedIdentityRegistrationEmailMissing=Email is not provided. Use another provider to create account please. - -loginTitle=Log in to -loginOauthTitle=Temporary access. -loginOauthTitleHtml=Temporary access requested. Login to grant access. -loginForgot=Forgot - -loginTotpTitle=Mobile Authenticator Setup -loginTotpStep1=Install FreeOTP or Google Authenticator on your mobile -loginTotpStep2=Open the application and scan the barcode or enter the key -loginTotpStep3=Enter the one-time code provided by the application and click Submit to finish the setup -loginTotpOneTime=One-time code - -loginProfileTitle=Update Account Information -loginProfileWarning=Your account is not enabled because you need to update your account information. -loginProfileWarningFollow=Please follow the steps below. -loginProfileError=Some required fields are empty or incorrect. -loginProfileErrorSteps=Please correct the fields in red. - -oauthGrantTitle=OAuth Grant -oauthGrantTitleHtml=Temporary access requested -oauthGrantTerms=Keycloak Central Login and Google will use this information in accordance with their respective terms of service and privacy policies. -oauthGrantRequest=Do you grant these access privileges? -oauthGrantLoginRequest=Do you grant access? - -emailVerifyTitle=Email verification -emailVerifyInstr=An email with instructions to verify your email address has been sent to you. -emailVerifyInstrQ=Haven't received a verification code in your email? -emailVerifyClick=Click here -emailVerifyResend=to re-send the email. -emailVerified=Email verified - -error=A system error has occured, contact admin -errorTitle=We're sorry... -errorTitleHtml=We're sorry ... -errorGenericMsg=Something happened and we could not process your request. -actionWarningHeader=Your account is not enabled. -actionTotpWarning=You need to set up Mobile Authenticator to activate your account. -actionProfileWarning=You need to update your user profile to activate your account. -actionPasswordWarning=You need to change your password to activate your account. -actionEmailWarning=You need to verify your email address to activate your account. -actionFollow=Please fill in the fields below. - -errorKerberosLogin=Kerberos ticket not available. Authenticate with password. - -successHeader=Success! -errorHeader=Error! - -# Forgot password part - -emailForgotHeader=Forgot Your Password? -backToLogin=« Back to Login -backToApplication=« Back to Application -emailUpdateHeader=Update password -emailSent=You should receive an email shortly with further instructions. -emailSendError=Failed to send email, please try again later -emailError=Invalid email. -emailErrorInfo=Please, fill in the fields again. -emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password. - -accountUpdated=Your account has been updated -accountPasswordUpdated=Your password has been updated \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages_de.properties new file mode 100644 index 0000000000..70d8aba28c --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages_de.properties @@ -0,0 +1,148 @@ +doLogIn=Anmelden +doRegister=Registrieren +doCancel=Abbrechen +doSubmit=Absenden +doYes=Ja +doNo=Nein +doForgotPassword=Passwort vergessen? +doClickHere=hier klicken + +registerWithTitle=Registrierung bei {0} +registerWithTitleHtml=Registrierung bei {0} +loginTitle=Anmeldung bei {0} +loginTitleHtml=Anmeldung bei {0} +loginOauthTitle=Temporärer zugriff auf {0} +loginOauthTitleHtml=Temporärer zugriff auf {0} angefordert von {1}. +loginTotpTitle=Mobile Authentifizierung Einrichten +loginProfileTitle=Benutzer Konto Informatinen aktualisieren +oauthGrantTitle=OAuth gewähren +oauthGrantTitleHtml=Temporärer zugriff auf {0} angefordert von {1}. +errorTitle=Es tut uns leid... +errorTitleHtml=Es tut uns leid... +emailVerifyTitle=E-Mail verifizieren +emailForgotTitle=Passwort vergessen? +updatePasswordTitle=Passwort aktualisieren + +noAccount=Neuer Benutzer? +username=Benutzername +usernameOrEmail=Benutzername oder E-Mail +firstName=Vorname +fullName=Name +lastName=Nachname +email=E-Mail +password=Passwort +passwordConfirm=Passwort bestätigen +passwordNew=Neues Passwort +passwordNewConfirm=Neues Passwort bestätigen +rememberMe=Angemeldet bleiben +authenticatorCode=One-time Code +street=Strasse +region=Staat, Provinz, Region +postal_code=PLZ +locality=Stadt oder Ortschaft +country=Land + +loginTotpStep1=Installieren Sie FreeOTP oder Google Authenticator auf Ihrem Smartphone. +loginTotpStep2=Öffnen Sie die Applikation und scannen Sie den Barcode oder geben sie den Code ein. +loginTotpStep3=Geben Sie den One-time Code welcher die Applikation generiert hat ein und klicken Sie auf Absenden. +loginTotpOneTime=One-time Code + +oauthGrantRequest=Do you grant these access privileges? + +emailVerifyInstruction1=Ein E-Mail mit weitern Anweisungen wurde an Sie versendet. +emailVerifyInstruction2=Falls Sie kein E-Mail erhalten haben, dann können Sie +emailVerifyInstruction3=um ein neues E-Mail zu verschicken. + +backToLogin=« Zurück zur Anmeldung +backToApplication=« Zurück zur Applikation + +emailInstruction=Geben Sie ihren Benutzernamen oder E-Mail Adresse ein und klicken Sie auf Absenden. Danach werden wir ihnen ein E-Mail mit weiteren Instruktionen zusenden. + +invalidUserMessage=Ungültiger Benutzername oder Passwort. +invalidEmailMessage=Ungültige E-Mail Adresse. +accountDisabledMessage=Benutzerkonto ist gesperrt, bitte kontaktieren Sie den Admin. +accountTemporarilyDisabledMessage=Benutzerkonto ist temporär gespert, bitte kontaktieren Sie den Admin oder versuchen Sie es später nocheinmal. +expiredCodeMessage=Zeitüberschreitung bei der Anmeldung. Bitter melden Sie sich erneut an. + +missingFirstNameMessage=Bitte geben Sie einen Vornamen ein. +missingLastNameMessage=Bitte geben Sie einen Nachnamen ein. +missingEmailMessage=Bitte geben Sie eine E-Mail Adresse ein. +missingUsernameMessage=Bitte geben Sie einen Benutzernamen ein. +missingPasswordMessage=Bitte geben Sie ein Passwort ein. +missingTotpMessage=Bitte geben Sie den One-time Code ein. +notMatchPasswordMessage=Passwörter sind nicht identisch. + +invalidPasswordExistingMessage=Das aktuelle Passwort is ungültig. +invalidPasswordConfirmMessage=Die Passwort bestätigung ist nicht identisch. +invalidTotpMessage=Ungültiger One-time Code. + +usernameExistsMessage=Benutzermane exisitert bereits. +emailExistsMessage=E-Mail existiert bereits. + +federatedIdentityRegistrationEmailMissingMessage=Die E-Mail Adresse ist nicht vorhanden. Bitte verwenden Sie einen anderen Provider um das Benutzerkonto zu erstellen. +federatedIdentityEmailExistsMessage=Es exisitert bereits ein Benutzer mit dieser E-Mail Adresse. Bitte melden Sie sich bei der Benutzerverwaltung an um das Benutzerkonto zu verknüpfen. +federatedIdentityUsernameExistsMessage=Es exisitert bereits ein Benutzer mit diesem Benutzernamen. Bitte melden Sie sich bei der Benutzerverwaltung an um das Benutzerkonto zu verknüpfen. + +configureTotpMessage=Sie müssen eine Mobile Authentifizierung einrichten um das Benutzerkonto zu aktivieren. +updateProfileMessage=Sie müssen ihr Benutzerkonto aktualisieren um das Benutzerkonto zu aktivieren. +updatePasswordMessage=Sie müssen ihr Passwort ändern um das Benutzerkonto zu aktivieren. +verifyEmailMessage=Sie müssen ihre E-Mail Adresse verifizieren um das Benutzerkonto zu aktivieren. + +emailSentMessage=Sie sollten in kürze ein E-Mail mit weiteren Instruktionen erhalten. +emailSendErrorMessage=Das E-Mail konnte nicht versendet werden, bitte versuchen Sie es später nochmals. + +accountUpdatedMessage=Ihr Benutzerkonto wurde aktualisiert. +accountPasswordUpdatedMessage=Ihr Passwort wurde aktualisiert. + +noAccessMessage=Kein Zugriff + +invalidPasswordMinLengthMessage=Ungültiges Passwort: minimum länge {0}. +invalidPasswordMinDigitsMessage=Ungültiges Passwort: muss mindestens {0} Zahl(en) beinhalten. +invalidPasswordMinLowerCaseCharsMessage=Ungültiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten. +invalidPasswordMinUpperCaseCharsMessage=Ungültiges Passwort: muss mindestens {0} Grossbuchstaben beinhalten. +invalidPasswordMinSpecialCharsMessage=Ungültiges Passwort: muss mindestens {0} Spezialzeichen beinhalten. +invalidPasswordNotUsernameMessage=Ungültiges Passwort\: darf nicht gleich sein wie Benutzername. + +failedToProcessResponseMessage=Konnte Response nicht verarbeiten. +httpsRequiredMessage=HTTPS erforderlich. +realmNotEnabledMessage=Realm nicht aktiviert. +invalidRequestMessage=Ungültiger Request. +unknownLoginRequesterMessage=Ungültiger login requester +loginRequesterNotEnabledMessage=Login requester nicht aktiviert. +bearerOnlyMessage=Bearer-only Applikationen könne sich nicht via Browser anmelden. +directGrantsOnlyMessage=Direct-grants-only Clients könne sich nicht via Browser anmelden. +invalidRedirectUriMessage=Ungültige redirect uri. +unsupportedNameIdFormatMessage=Nicht unterstütztes NameIDFormat. +invlidRequesterMessage=Ungültiger requester. +registrationNotAllowedMessage=Registrierung nicht erlaubt. + +permissionNotApprovedMessage=Berechtigung nicht bestätigt. +noRelayStateInResponseMessage=Kein relay state in der Antwort von dem Identity Provider [{0}]. +identityProviderAlreadyLinkedMessage=Die Identität welche von dem Identity Provider [{0}] zurückgegeben wurde, ist bereits mit einem anderen Benutzer verknüpft. +insufficientPermissionMessage=Nicht genügtend Rechte um die Identität zu verknüpfen. +couldNotProceedWithAuthenticationRequestMessage=Konnte den Authentifizierungs Request nicht weiter verarbeiten. +couldNotObtainTokenMessage=Konnte kein token vom Identity Provider [{0}] entnehmen. +unexpectedErrorRetrievingTokenMessage=Unerwarteter Fehler während dem Empfang des Token von dem Identity Provider [{0}]. +unexpectedErrorHandlingResponseMessage=Unerwarteter Fehler während der bearbeitung des Respons vom Identity Provider [{0}]. +identityProviderAuthenticationFailedMessage=Authentifizierung Fehlgeschlagen. Konnte sich mit dem Identity Provider [{0}] nicht authentifizieren. +couldNotSendAuthenticationRequestMessage=Konnte Authentifizierungs Request nicht an den Identity Provider [{0}] schiken. +unexpectedErrorHandlingRequestMessage=Unerwarteter Fehler während der bearbeitung des Requests zum Identity Provider [{0}]. +invalidAccessCodeMessage=Ungültiger Access-Code. +sessionNotActiveMessage=Session nicht aktiv. +unknownCodeMessage=Unbekannter Code, bitte melden Sie sich erneut über die Applikation an. +invalidCodeMessage=Ungültiger Code, bitte melden Sie sich erneut über die Applikation an. +identityProviderUnexpectedErrorMessage=Unerwarteter Fehler während der Authentifizierung mit dem Identity Provider. +identityProviderNotFoundMessage=Konnte kein Identity Provider mit der Idenität [{0}] finden. +realmSupportsNoCredentialsMessage=Realm [{0}] unterstützt keine Credential Typen.. +identityProviderNotUniqueMessage=Realm [{0}] unterstütz mehrere Identity Providers. + +invalidParameterMessage=Invalid parameter\: {0} +missingParameterMessage=Missing parameter\: {0} +clientNotFoundMessage=Client not found. + +emailVerifiedMessage=Ihr E-Mail Addresse wurde erfolgreich verifiziert. + +locale_de=Deutsch +locale_en=Englisch + +poweredByKeycloak=Powered by Keycloak \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/login/base/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/login/base/messages/messages_en.properties new file mode 100755 index 0000000000..6f48c59dca --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/login/base/messages/messages_en.properties @@ -0,0 +1,146 @@ +doLogIn=Log in +doRegister=Register +doCancel=Cancel +doSubmit=Submit +doYes=Yes +doNo=No +doForgotPassword=Forgot Password? +doClickHere=Click here + +registerWithTitle=Register with {0} +registerWithTitleHtml=Register with {0} +loginTitle=Log in to {0} +loginTitleHtml=Log in to {0} +loginOauthTitle=Temporary access for {0} +loginOauthTitleHtml=Temporary access for {0} requested by {1}. +loginTotpTitle=Mobile Authenticator Setup +loginProfileTitle=Update Account Information +oauthGrantTitle=OAuth Grant +oauthGrantTitleHtml=Temporary access for {0} requested by {1}. +errorTitle=We're sorry... +errorTitleHtml=We're sorry ... +emailVerifyTitle=Email verification +emailForgotTitle=Forgot Your Password? +updatePasswordTitle=Update password + +noAccount=New user? +username=Username +usernameOrEmail=Username or email +firstName=First name +fullName=Full name +lastName=Last name +email=Email +password=Password +passwordConfirm=Confirm password +passwordNew=New Password +passwordNewConfirm=New Password confirmation +rememberMe=Remember me +authenticatorCode=One-time code +street=Street +locality=City or Locality +region=State, Province, or Region +postal_code=Zip or Postal code +country=Country + +loginTotpStep1=Install FreeOTP or Google Authenticator on your mobile +loginTotpStep2=Open the application and scan the barcode or enter the key +loginTotpStep3=Enter the one-time code provided by the application and click Submit to finish the setup +loginTotpOneTime=One-time code + +oauthGrantRequest=Do you grant these access privileges? + +emailVerifyInstruction1=An email with instructions to verify your email address has been sent to you. +emailVerifyInstruction2=Haven't received a verification code in your email? +emailVerifyInstruction3=to re-send the email. + +backToLogin=« Back to Login + +emailInstruction=Enter your username or email address and we will send you instructions on how to create a new password. + +invalidUserMessage=Invalid username or password. +invalidEmailMessage=Invalid email address. +accountDisabledMessage=Account is disabled, contact admin. +accountTemporarilyDisabledMessage=Account is temporarily disabled, contact admin or try again later. +expiredCodeMessage=Login timeout. Please login again. + +missingFirstNameMessage=Please specify first name. +missingLastNameMessage=Please specify last name. +missingEmailMessage=Please specify email. +missingUsernameMessage=Please specify username. +missingPasswordMessage=Please specify password. +missingTotpMessage=Please specify authenticator code. +notMatchPasswordMessage=Passwords don't match. + +invalidPasswordExistingMessage=Invalid existing password. +invalidPasswordConfirmMessage=Password confirmation doesn't match. +invalidTotpMessage=Invalid authenticator code. + +usernameExistsMessage=Username already exists. +emailExistsMessage=Email already exists. + +federatedIdentityEmailExistsMessage=User with email already exists. Please login to account management to link the account. +federatedIdentityUsernameExistsMessage=User with username already exists. Please login to account management to link the account. + +configureTotpMessage=You need to set up Mobile Authenticator to activate your account. +updateProfileMessage=You need to update your user profile to activate your account. +updatePasswordMessage=You need to change your password to activate your account. +verifyEmailMessage=You need to verify your email address to activate your account. + +emailSentMessage=You should receive an email shortly with further instructions. +emailSendErrorMessage=Failed to send email, please try again later + +accountUpdatedMessage=Your account has been updated +accountPasswordUpdatedMessage=Your password has been updated + +noAccessMessage=No access + +invalidPasswordMinLengthMessage=Invalid password: minimum length {0} +invalidPasswordMinDigitsMessage=Invalid password: must contain at least {0} numerical digits +invalidPasswordMinLowerCaseCharsMessage=Invalid password: must contain at least {0} lower case characters +invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least {0} upper case characters +invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters +invalidPasswordNotUsernameMessage=Invalid password\: must not be equal to the username + +failedToProcessResponseMessage=Failed to process response +httpsRequiredMessage=HTTPS required +realmNotEnabledMessage=Realm not enabled +invalidRequestMessage=Invalid Request +unknownLoginRequesterMessage=Unknown login requester +loginRequesterNotEnabledMessage=Login requester not enabled +bearerOnlyMessage=Bearer-only applications are not allowed to initiate browser login +directGrantsOnlyMessage=Direct-grants-only clients are not allowed to initiate browser login +invalidRedirectUriMessage=Invalid redirect uri +unsupportedNameIdFormatMessage=Unsupported NameIDFormat +invlidRequesterMessage=Invalid requester +registrationNotAllowedMessage=Registration not allowed + +permissionNotApprovedMessage=Permission not approved. +noRelayStateInResponseMessage=No relay state in response from identity provider [{0}]. +identityProviderAlreadyLinkedMessage=The identity returned by the identity provider [{0}] is already linked to another user. +insufficientPermissionMessage=Insufficient permissions to link identities. +couldNotProceedWithAuthenticationRequestMessage=Could not proceed with authentication request to identity provider. +couldNotObtainTokenMessage=Could not obtain token from identity provider [{0}]. +unexpectedErrorRetrievingTokenMessage=Unexpected error when retrieving token from identity provider [{0}]. +unexpectedErrorHandlingResponseMessage=Unexpected error when handling response from identity provider [{0}]. +identityProviderAuthenticationFailedMessage=Authentication failed. Could not authenticate with identity provider [{0}]. +couldNotSendAuthenticationRequestMessage=Could not send authentication request to identity provider [{0}]. +unexpectedErrorHandlingRequestMessage=Unexpected error when handling authentication request to identity provider [{0}]. +invalidAccessCodeMessage=Invalid access code. +sessionNotActiveMessage=Session not active. +unknownCodeMessage=Unknown code, please login again through your application. +invalidCodeMessage=Invalid code, please login again through your application. +identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider +identityProviderNotFoundMessage=Could not find an identity provider with the identifier [{0}]. +realmSupportsNoCredentialsMessage=Realm [{0}] does not support any credential type. +identityProviderNotUniqueMessage=Realm [{0}] supports multiple identity providers. Could not determine which identity provider should be used to authenticate with. +emailVerifiedMessage=Your email address has been verified. + +locale_de=German +locale_en=English + +poweredByKeycloak=Powered by Keycloak +backToApplication=« Back to Application +missingParameterMessage=Missing parameters\: {0} +clientNotFoundMessage=Client not found. +invalidParameterMessage=Invalid parameter\: {0} +federatedIdentityRegistrationEmailMissingMessage=Email is not provided. Use another provider to create account please. diff --git a/forms/common-themes/src/main/resources/theme/login/base/register.ftl b/forms/common-themes/src/main/resources/theme/login/base/register.ftl index 492fbabd52..8dcdcea481 100755 --- a/forms/common-themes/src/main/resources/theme/login/base/register.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/register.ftl @@ -1,9 +1,9 @@ <#import "template.ftl" as layout> <@layout.registrationLayout; section> <#if section = "title"> - ${rb.registerWith} ${realm.name} + ${formatter.format(rb.registerWithTitle,(realm.name!''))} <#elseif section = "header"> - ${rb.registerWith} ${realm.name} + ${formatter.format(rb.registerWithTitleHtml,(realm.name!''))} <#elseif section = "form">
<#if !realm.registrationEmailAsUsername> @@ -116,7 +116,7 @@
- +
diff --git a/forms/common-themes/src/main/resources/theme/login/base/template.ftl b/forms/common-themes/src/main/resources/theme/login/base/template.ftl index 49c02469b2..c09271da62 100644 --- a/forms/common-themes/src/main/resources/theme/login/base/template.ftl +++ b/forms/common-themes/src/main/resources/theme/login/base/template.ftl @@ -21,6 +21,19 @@ + <#if realm.internationalizationEnabled> + + @@ -31,6 +44,15 @@
<#nested "header">
+ <#if realm.internationalizationEnabled> +
+ +
+
<#if displayMessage && message?has_content> 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 df43d16fb2..21972b2c00 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 @@ -5,9 +5,12 @@ import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.email.freemarker.beans.EventBean; import org.keycloak.events.Event; +import org.keycloak.events.EventType; 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.TextFormatterBean; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -17,10 +20,7 @@ import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; +import java.util.*; /** * @author Stian Thorgersen @@ -56,7 +56,7 @@ public class FreeMarkerEmailProvider implements EmailProvider { Map attributes = new HashMap(); attributes.put("event", new EventBean(event)); - send("passwordResetSubject", "event-" + event.getType().toString().toLowerCase() + ".ftl", attributes); + send(toCamelCase(event.getType()) + "Subject", "event-" + event.getType().toString().toLowerCase() + ".ftl", attributes); } @Override @@ -81,8 +81,12 @@ public class FreeMarkerEmailProvider implements EmailProvider { try { ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL); - - String subject = theme.getMessages().getProperty(subjectKey); + Locale locale = LocaleHelper.getLocale(realm, user); + attributes.put("locale", locale); + Properties rb = theme.getMessages(locale); + attributes.put("rb", rb); + attributes.put("formatter", new TextFormatterBean(locale)); + String subject = rb.getProperty(subjectKey); String body = freeMarker.processTemplate(attributes, template, theme); send(subject, body); @@ -150,4 +154,12 @@ public class FreeMarkerEmailProvider implements EmailProvider { public void close() { } + private String toCamelCase(EventType event){ + StringBuilder sb = new StringBuilder("event"); + for(String s : event.name().toString().toLowerCase().split("_")){ + sb.append(s.substring(0,1).toUpperCase()).append(s.substring(1)); + } + return sb.toString(); + } + } diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index 11c26fe767..36a5252515 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -7,6 +7,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -22,6 +23,8 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setUriInfo(UriInfo uriInfo); + public LoginFormsProvider setHttpHeaders(HttpHeaders httpHeaders); + public Response createResponse(UserModel.RequiredAction action); public Response createLogin(); @@ -45,11 +48,11 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested); public LoginFormsProvider setAccessRequest(String message); - public LoginFormsProvider setError(String message); + public LoginFormsProvider setError(String message, Object ... parameters); - public LoginFormsProvider setSuccess(String message); + public LoginFormsProvider setSuccess(String message, Object ... parameters); - public LoginFormsProvider setWarning(String message); + public LoginFormsProvider setWarning(String message, Object ... parameters); public LoginFormsProvider setUser(UserModel user); 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 be59af04a3..14579b9378 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 @@ -5,11 +5,8 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.keycloak.OAuth2Constants; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; -import org.keycloak.freemarker.BrowserSecurityHeaderSetup; -import org.keycloak.freemarker.FreeMarkerException; -import org.keycloak.freemarker.FreeMarkerUtil; -import org.keycloak.freemarker.Theme; -import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.freemarker.*; +import org.keycloak.freemarker.beans.TextFormatterBean; import org.keycloak.login.LoginFormsPages; import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.freemarker.model.ClientBean; @@ -33,23 +30,17 @@ import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.flows.Urls; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; +import javax.ws.rs.core.*; import java.io.IOException; import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.text.MessageFormat; +import java.util.*; import java.util.concurrent.TimeUnit; /** * @author Stian Thorgersen */ -public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { + public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class); @@ -63,6 +54,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private Map httpResponseHeaders = new HashMap(); private String accessRequestMessage; private URI actionUri; + private Object[] parameters; private String message; private MessageType messageType = MessageType.ERROR; @@ -80,6 +72,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private UriInfo uriInfo; + private HttpHeaders httpHeaders; + public FreeMarkerLoginFormsProvider(KeycloakSession session, FreeMarkerUtil freeMarker) { this.session = session; this.freeMarker = freeMarker; @@ -95,21 +89,27 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } + @Override + public LoginFormsProvider setHttpHeaders(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + return this; + } + public Response createResponse(UserModel.RequiredAction action) { String actionMessage; LoginFormsPages page; switch (action) { case CONFIGURE_TOTP: - actionMessage = Messages.ACTION_WARN_TOTP; + actionMessage = Messages.CONFIGURE_TOTP; page = LoginFormsPages.LOGIN_CONFIG_TOTP; break; case UPDATE_PROFILE: - actionMessage = Messages.ACTION_WARN_PROFILE; + actionMessage = Messages.UPDATE_PROFILE; page = LoginFormsPages.LOGIN_UPDATE_PROFILE; break; case UPDATE_PASSWORD: - actionMessage = Messages.ACTION_WARN_PASSWD; + actionMessage = Messages.UPDATE_PASSWORD; page = LoginFormsPages.LOGIN_UPDATE_PASSWORD; break; case VERIFY_EMAIL: @@ -123,10 +123,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendVerifyEmail(link, expiration); } catch (EmailException e) { logger.error("Failed to send verification email", e); - return setError("emailSendError").createErrorPage(); + return setError(Messages.EMAIL_SENT_ERROR).createErrorPage(); } - actionMessage = Messages.ACTION_WARN_EMAIL; + actionMessage = Messages.VERIFY_EMAIL; page = LoginFormsPages.LOGIN_VERIFY_EMAIL; break; default: @@ -175,8 +175,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } Properties messages; + Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, httpHeaders); + if(locale != null){ + attributes.put("locale", locale); + attributes.put("formatter", new TextFormatterBean(locale)); + } try { - messages = theme.getMessages(); + messages = theme.getMessages(locale); attributes.put("rb", messages); } catch (IOException e) { logger.warn("Failed to load messages", e); @@ -184,7 +189,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } if (message != null) { - attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType)); + String formattedMessage; + if(messages.containsKey(message)){ + formattedMessage = new MessageFormat(messages.getProperty(message).replace("'","''"),locale).format(parameters); + }else{ + formattedMessage = message; + } + attributes.put("message", new MessageBean(formattedMessage, messageType)); } if (page == LoginFormsPages.OAUTH_GRANT) { // for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param @@ -233,6 +244,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { for (Map.Entry entry : httpResponseHeaders.entrySet()) { builder.header(entry.getKey(), entry.getValue()); } + LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri, realm.getName())); return builder.build(); } catch (FreeMarkerException e) { logger.error("Failed to process template", e); @@ -277,21 +289,24 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return createResponse(LoginFormsPages.CODE); } - public FreeMarkerLoginFormsProvider setError(String message) { + public FreeMarkerLoginFormsProvider setError(String message, Object ... parameters) { this.message = message; this.messageType = MessageType.ERROR; + this.parameters = parameters; return this; } - public FreeMarkerLoginFormsProvider setSuccess(String message) { + public FreeMarkerLoginFormsProvider setSuccess(String message, Object ... parameters) { this.message = message; this.messageType = MessageType.SUCCESS; + this.parameters = parameters; return this; } - public FreeMarkerLoginFormsProvider setWarning(String message) { + public FreeMarkerLoginFormsProvider setWarning(String message, Object ... parameters) { this.message = message; this.messageType = MessageType.WARNING; + this.parameters = parameters; return this; } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java index e4ac27f406..3e36d08c9f 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java @@ -25,6 +25,8 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; import org.keycloak.representations.idm.CredentialRepresentation; +import java.util.Set; + /** * @author Stian Thorgersen */ @@ -60,6 +62,14 @@ public class RealmBean { return realm.isRememberMe(); } + public boolean isInternationalizationEnabled() { + return realm.isInternationalizationEnabled(); + } + + public Set getSupportedLocales(){ + return realm.getSupportedLocales(); + } + public boolean isPassword() { for (RequiredCredentialModel r : realm.getRequiredCredentials()) { if (r.getType().equals(CredentialRepresentation.PASSWORD)) { diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java index 26d67a3bc8..259b09856c 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java @@ -86,6 +86,10 @@ public class UrlBean { return Urls.loginActionEmailVerification(baseURI, realm).toString(); } + public String getLocaleCookiePath(){ + return Urls.localeCookiePath(baseURI, realm); + } + public String getOauthAction() { if (this.actionuri != null) { return this.actionuri.getPath(); diff --git a/model/api/src/main/java/org/keycloak/models/ModelException.java b/model/api/src/main/java/org/keycloak/models/ModelException.java index 1c2f068dad..d3431b056b 100644 --- a/model/api/src/main/java/org/keycloak/models/ModelException.java +++ b/model/api/src/main/java/org/keycloak/models/ModelException.java @@ -5,6 +5,8 @@ package org.keycloak.models; */ public class ModelException extends RuntimeException { + private Object[] parameters; + public ModelException() { } @@ -12,6 +14,11 @@ public class ModelException extends RuntimeException { super(message); } + public ModelException(String message, Object ... parameters) { + super(message); + this.parameters = parameters; + } + public ModelException(String message, Throwable cause) { super(message, cause); } @@ -20,4 +27,11 @@ public class ModelException extends RuntimeException { super(cause); } + public Object[] getParameters() { + return parameters; + } + + public void setParameters(Object[] parameters) { + this.parameters = parameters; + } } diff --git a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java index 3651e9d3f4..cfcc108db8 100755 --- a/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java +++ b/model/api/src/main/java/org/keycloak/models/PasswordPolicy.java @@ -1,5 +1,6 @@ package org.keycloak.models; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -9,6 +10,13 @@ import java.util.List; */ public class PasswordPolicy { + public static final String INVALID_PASSWORD_MIN_LENGTH_MESSAGE = "invalidPasswordMinLengthMessage"; + public static final String INVALID_PASSWORD_MIN_DIGITS_MESSAGE = "invalidPasswordMinDigitsMessage"; + public static final String INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE = "invalidPasswordMinLowerCaseCharsMessage"; + public static final String INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE = "invalidPasswordMinUpperCaseCharsMessage"; + public static final String INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE = "invalidPasswordMinSpecialCharsMessage"; + public static final String INVALID_PASSWORD_NOT_USERNAME = "invalidPasswordNotUsernameMessage"; + private List policies; private String policyString; @@ -76,9 +84,9 @@ public class PasswordPolicy { return -1; } - public String validate(String username, String password) { + public Error validate(String username, String password) { for (Policy p : policies) { - String error = p.validate(username, password); + Error error = p.validate(username, password); if (error != null) { return error; } @@ -87,7 +95,25 @@ public class PasswordPolicy { } private static interface Policy { - public String validate(String username, String password); + public Error validate(String username, String password); + } + + public static class Error{ + private String message; + private Object[] parameters; + + private Error(String message, Object ... parameters){ + this.message = message; + this.parameters = parameters; + } + + public String getMessage() { + return message; + } + + public Object[] getParameters() { + return parameters; + } } private static class HashIterations implements Policy { @@ -99,7 +125,7 @@ public class PasswordPolicy { } @Override - public String validate(String username, String password) { + public Error validate(String username, String password) { return null; } } @@ -111,8 +137,8 @@ public class PasswordPolicy { } @Override - public String validate(String username, String password) { - return username.equals(password) ? "Invalid password: must not be equal to the username" : null; + public Error validate(String username, String password) { + return username.equals(password) ? new Error(INVALID_PASSWORD_NOT_USERNAME) : null; } } @@ -125,8 +151,8 @@ public class PasswordPolicy { } @Override - public String validate(String username, String password) { - return password.length() < min ? "Invalid password: minimum length " + min : null; + public Error validate(String username, String password) { + return password.length() < min ? new Error(INVALID_PASSWORD_MIN_LENGTH_MESSAGE, min) : null; } } @@ -139,14 +165,14 @@ public class PasswordPolicy { } @Override - public String validate(String username, String password) { + public Error validate(String username, String password) { int count = 0; for (char c : password.toCharArray()) { if (Character.isDigit(c)) { count++; } } - return count < min ? "Invalid password: must contain at least " + min + " numerical digits" : null; + return count < min ? new Error(INVALID_PASSWORD_MIN_DIGITS_MESSAGE, min) : null; } } @@ -159,14 +185,14 @@ public class PasswordPolicy { } @Override - public String validate(String username, String password) { + public Error validate(String username, String password) { int count = 0; for (char c : password.toCharArray()) { if (Character.isLowerCase(c)) { count++; } } - return count < min ? "Invalid password: must contain at least " + min + " lower case characters": null; + return count < min ? new Error(INVALID_PASSWORD_MIN_LOWER_CASE_CHARS_MESSAGE, min): null; } } @@ -179,14 +205,14 @@ public class PasswordPolicy { } @Override - public String validate(String username, String password) { + public Error validate(String username, String password) { int count = 0; for (char c : password.toCharArray()) { if (Character.isUpperCase(c)) { count++; } } - return count < min ? "Invalid password: must contain at least " + min + " upper case characters" : null; + return count < min ? new Error(INVALID_PASSWORD_MIN_UPPER_CASE_CHARS_MESSAGE, min) : null; } } @@ -199,14 +225,14 @@ public class PasswordPolicy { } @Override - public String validate(String username, String password) { + public Error validate(String username, String password) { int count = 0; for (char c : password.toCharArray()) { if (!Character.isLetterOrDigit(c)) { count++; } } - return count < min ? "Invalid password: must contain at least " + min + " special characters" : null; + return count < min ? new Error(INVALID_PASSWORD_MIN_SPECIAL_CHARS_MESSAGE, min) : null; } } diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index ab260a13a3..7308565573 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -244,4 +244,11 @@ public interface RealmModel extends RoleContainerModel { ClientModel findClientById(String id); boolean isIdentityFederationEnabled(); + + boolean isInternationalizationEnabled(); + void setInternationalizationEnabled(boolean enabled); + Set getSupportedLocales(); + void setSupportedLocales(Set locales); + String getDefaultLocale(); + void setDefaultLocale(String locale); } diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index a360f958a4..1016341e3a 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -323,8 +323,8 @@ public class UserFederationManager implements UserProvider { public void updateCredential(RealmModel realm, UserModel user, UserCredentialModel credential) { if (credential.getType().equals(UserCredentialModel.PASSWORD)) { if (realm.getPasswordPolicy() != null) { - String error = realm.getPasswordPolicy().validate(user.getUsername(), credential.getValue()); - if (error != null) throw new ModelException(error); + PasswordPolicy.Error error = realm.getPasswordPolicy().validate(user.getUsername(), credential.getValue()); + if (error != null) throw new ModelException(error.getMessage(), error.getParameters()); } } user.updateCredential(credential); 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 1047ad252b..ee8ce80d0c 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -13,6 +13,7 @@ public interface UserModel { 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 getId(); diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index 38baa89319..678fb3326a 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -65,6 +65,10 @@ public class RealmEntity extends AbstractIdentifiableEntity { private String adminAppId; + private boolean internationalizationEnabled; + private List supportedLocales = new ArrayList(); + private String defaultLocale; + public String getName() { return name; } @@ -407,6 +411,30 @@ public class RealmEntity extends AbstractIdentifiableEntity { public void setCertificatePem(String certificatePem) { this.certificatePem = certificatePem; } + + public boolean isInternationalizationEnabled() { + return internationalizationEnabled; + } + + public void setInternationalizationEnabled(boolean internationalizationEnabled) { + this.internationalizationEnabled = internationalizationEnabled; + } + + public List getSupportedLocales() { + return supportedLocales; + } + + public void setSupportedLocales(List supportedLocales) { + this.supportedLocales = supportedLocales; + } + + public String getDefaultLocale() { + return defaultLocale; + } + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 686eede868..736aff8b12 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -155,6 +155,10 @@ public class ModelToRepresentation { rep.addIdentityProvider(toRepresentation(provider)); } + rep.setInternationalizationEnabled(realm.isInternationalizationEnabled()); + rep.getSupportedLocales().addAll(realm.getSupportedLocales()); + rep.setDefaultLocale(realm.getDefaultLocale()); + return rep; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index ce87adae13..c2090d3ce9 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -243,6 +243,16 @@ public class RepresentationToModel { UserModel user = createUser(session, newRealm, userRep, appMap); } } + + if(rep.isInternationalizationEnabled() != null){ + newRealm.setInternationalizationEnabled(rep.isInternationalizationEnabled()); + } + if(rep.getSupportedLocales() != null){ + newRealm.setSupportedLocales(new HashSet(rep.getSupportedLocales())); + } + if(rep.getDefaultLocale() != null){ + newRealm.setDefaultLocale(rep.getDefaultLocale()); + } } public static void updateRealm(RealmRepresentation rep, RealmModel realm) { @@ -304,6 +314,16 @@ public class RepresentationToModel { if ("GENERATE".equals(rep.getPublicKey())) { KeycloakModelUtils.generateRealmKeys(realm); } + + if(rep.isInternationalizationEnabled() != null){ + realm.setInternationalizationEnabled(rep.isInternationalizationEnabled()); + } + if(rep.getSupportedLocales() != null){ + realm.setSupportedLocales(new HashSet(rep.getSupportedLocales())); + } + if(rep.getDefaultLocale() != null){ + realm.setDefaultLocale(rep.getDefaultLocale()); + } } // Basic realm stuff diff --git a/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java b/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java index 7242c554df..3bde361113 100644 --- a/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java +++ b/model/api/src/test/java/org/keycloak/models/PasswordPolicyTest.java @@ -11,62 +11,72 @@ public class PasswordPolicyTest { @Test public void testLength() { PasswordPolicy policy = new PasswordPolicy("length"); - Assert.assertEquals("Invalid password: minimum length 8", policy.validate("jdoe", "1234567")); + Assert.assertEquals("invalidPasswordMinLengthMessage", policy.validate("jdoe", "1234567").getMessage()); + Assert.assertArrayEquals(new Object[]{8}, policy.validate("jdoe", "1234567").getParameters()); Assert.assertNull(policy.validate("jdoe", "12345678")); policy = new PasswordPolicy("length(4)"); - Assert.assertEquals("Invalid password: minimum length 4", policy.validate("jdoe", "123")); + Assert.assertEquals("invalidPasswordMinLengthMessage", policy.validate("jdoe", "123").getMessage()); + Assert.assertArrayEquals(new Object[]{4}, policy.validate("jdoe", "123").getParameters()); Assert.assertNull(policy.validate("jdoe", "1234")); } @Test public void testDigits() { PasswordPolicy policy = new PasswordPolicy("digits"); - Assert.assertEquals("Invalid password: must contain at least 1 numerical digits", policy.validate("jdoe", "abcd")); + Assert.assertEquals("invalidPasswordMinDigitsMessage", policy.validate("jdoe", "abcd").getMessage()); + Assert.assertArrayEquals(new Object[]{1}, policy.validate("jdoe", "abcd").getParameters()); Assert.assertNull(policy.validate("jdoe", "abcd1")); policy = new PasswordPolicy("digits(2)"); - Assert.assertEquals("Invalid password: must contain at least 2 numerical digits", policy.validate("jdoe", "abcd1")); + Assert.assertEquals("invalidPasswordMinDigitsMessage", policy.validate("jdoe", "abcd1").getMessage()); + Assert.assertArrayEquals(new Object[]{2}, policy.validate("jdoe", "abcd1").getParameters()); Assert.assertNull(policy.validate("jdoe", "abcd12")); } @Test public void testLowerCase() { PasswordPolicy policy = new PasswordPolicy("lowerCase"); - Assert.assertEquals("Invalid password: must contain at least 1 lower case characters", policy.validate("jdoe", "ABCD1234")); + Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policy.validate("jdoe", "ABCD1234").getMessage()); + Assert.assertArrayEquals(new Object[]{1}, policy.validate("jdoe", "ABCD1234").getParameters()); Assert.assertNull(policy.validate("jdoe", "ABcD1234")); policy = new PasswordPolicy("lowerCase(2)"); - Assert.assertEquals("Invalid password: must contain at least 2 lower case characters", policy.validate("jdoe", "ABcD1234")); + Assert.assertEquals("invalidPasswordMinLowerCaseCharsMessage", policy.validate("jdoe", "ABcD1234").getMessage()); + Assert.assertArrayEquals(new Object[]{2}, policy.validate("jdoe", "ABcD1234").getParameters()); Assert.assertNull(policy.validate("jdoe", "aBcD1234")); } @Test public void testUpperCase() { PasswordPolicy policy = new PasswordPolicy("upperCase"); - Assert.assertEquals("Invalid password: must contain at least 1 upper case characters", policy.validate("jdoe", "abcd1234")); + Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policy.validate("jdoe", "abcd1234").getMessage()); + Assert.assertArrayEquals(new Object[]{1}, policy.validate("jdoe", "abcd1234").getParameters()); Assert.assertNull(policy.validate("jdoe", "abCd1234")); policy = new PasswordPolicy("upperCase(2)"); - Assert.assertEquals("Invalid password: must contain at least 2 upper case characters", policy.validate("jdoe", "abCd1234")); + Assert.assertEquals("invalidPasswordMinUpperCaseCharsMessage", policy.validate("jdoe", "abCd1234").getMessage()); + Assert.assertArrayEquals(new Object[]{2}, policy.validate("jdoe", "abCd1234").getParameters()); Assert.assertNull(policy.validate("jdoe", "AbCd1234")); } @Test public void testSpecialChars() { PasswordPolicy policy = new PasswordPolicy("specialChars"); - Assert.assertEquals("Invalid password: must contain at least 1 special characters", policy.validate("jdoe", "abcd1234")); + Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policy.validate("jdoe", "abcd1234").getMessage()); + Assert.assertArrayEquals(new Object[]{1}, policy.validate("jdoe", "abcd1234").getParameters()); Assert.assertNull(policy.validate("jdoe", "ab&d1234")); policy = new PasswordPolicy("specialChars(2)"); - Assert.assertEquals("Invalid password: must contain at least 2 special characters", policy.validate("jdoe", "ab&d1234")); + Assert.assertEquals("invalidPasswordMinSpecialCharsMessage", policy.validate("jdoe", "ab&d1234").getMessage()); + Assert.assertArrayEquals(new Object[]{2}, policy.validate("jdoe", "ab&d1234").getParameters()); Assert.assertNull(policy.validate("jdoe", "ab&d-234")); } @Test public void testNotUsername() { PasswordPolicy policy = new PasswordPolicy("notUsername"); - Assert.assertEquals("Invalid password: must not be equal to the username", policy.validate("jdoe", "jdoe")); + Assert.assertEquals("invalidPasswordNotUsernameMessage", policy.validate("jdoe", "jdoe").getMessage()); Assert.assertNull(policy.validate("jdoe", "ab&d1234")); } diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index 5183b74735..99ab488fe4 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -1067,6 +1067,36 @@ public class RealmAdapter implements RealmModel { realm.setAccessCodeLifespanLogin(accessCodeLifespanLogin); } + @Override + public boolean isInternationalizationEnabled() { + return realm.isInternationalizationEnabled(); + } + + @Override + public void setInternationalizationEnabled(boolean enabled) { + realm.setInternationalizationEnabled(enabled); + } + + @Override + public Set getSupportedLocales() { + return new HashSet<>(realm.getSupportedLocales()); + } + + @Override + public void setSupportedLocales(Set locales) { + realm.setSupportedLocales(new ArrayList<>(locales)); + } + + @Override + public String getDefaultLocale() { + return realm.getDefaultLocale(); + } + + @Override + public void setDefaultLocale(String locale) { + realm.setDefaultLocale(locale); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index a58774d369..77b84e64aa 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -888,4 +888,39 @@ public class RealmAdapter implements RealmModel { public int hashCode() { return getId().hashCode(); } + + @Override + public boolean isInternationalizationEnabled() { + if (updated != null) return updated.isInternationalizationEnabled(); + return cached.isInternationalizationEnabled(); + } + + @Override + public void setInternationalizationEnabled(boolean enabled) { + getDelegateForUpdate(); + updated.setInternationalizationEnabled(enabled); + } + + @Override + public Set getSupportedLocales() { + if (updated != null) return updated.getSupportedLocales(); + return cached.getSupportedLocales(); + } + + @Override + public void setSupportedLocales(Set locales) { + getDelegateForUpdate(); + updated.setSupportedLocales(locales); + } + + @Override + public String getDefaultLocale() { + if (updated != null) return updated.getDefaultLocale(); + return cached.getDefaultLocale(); + } + + @Override + public void setDefaultLocale(String locale) { + updated.setDefaultLocale(locale); + } } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index dc062401c6..a4ce5bb653 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -83,6 +83,9 @@ public class CachedRealm { private Map realmRoles = new HashMap(); private Map applications = new HashMap(); private Map clients = new HashMap(); + private boolean internationalizationEnabled; + private Set supportedLocales = new HashSet(); + private String defaultLocale; public CachedRealm() { } @@ -164,6 +167,10 @@ public class CachedRealm { cache.addCachedOAuthClient(cachedApp); } + internationalizationEnabled = model.isInternationalizationEnabled(); + supportedLocales.addAll(model.getSupportedLocales()); + defaultLocale = model.getDefaultLocale(); + } @@ -353,4 +360,16 @@ public class CachedRealm { public List getIdentityProviders() { return identityProviders; } + + public boolean isInternationalizationEnabled() { + return internationalizationEnabled; + } + + public Set getSupportedLocales() { + return supportedLocales; + } + + public String getDefaultLocale() { + return defaultLocale; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index a17dd1791d..73add51137 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1238,4 +1238,36 @@ public class RealmAdapter implements RealmModel { return !this.realm.getIdentityProviders().isEmpty(); } + @Override + public boolean isInternationalizationEnabled() { + return realm.isInternationalizationEnabled(); + } + + @Override + public void setInternationalizationEnabled(boolean enabled) { + realm.setInternationalizationEnabled(enabled); + em.flush(); + } + + @Override + public Set getSupportedLocales() { + return realm.getSupportedLocales(); + } + + @Override + public void setSupportedLocales(Set locales) { + realm.setSupportedLocales(locales); + em.flush(); + } + + @Override + public String getDefaultLocale() { + return realm.getDefaultLocale(); + } + + @Override + public void setDefaultLocale(String locale) { + realm.setDefaultLocale(locale); + em.flush(); + } } \ No newline at end of file diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 1ba7a5573c..563791eaa0 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -137,6 +137,18 @@ public class RealmEntity { @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") protected List identityProviders = new ArrayList(); + @Column(name="INTERNATIONALIZATION_ENABLED") + protected boolean internationalizationEnabled; + + @ElementCollection + @Column(name="VALUE") + @CollectionTable(name="REALM_SUPPORTED_LOCALES", joinColumns={ @JoinColumn(name="REALM_ID") }) + protected Set supportedLocales = new HashSet(); + + @Column(name="DEFAULT_LOCALE") + protected String defaultLocale; + + public String getId() { return id; } @@ -452,5 +464,28 @@ public class RealmEntity { getIdentityProviders().add(entity); } + public boolean isInternationalizationEnabled() { + return internationalizationEnabled; + } + + public void setInternationalizationEnabled(boolean internationalizationEnabled) { + this.internationalizationEnabled = internationalizationEnabled; + } + + public Set getSupportedLocales() { + return supportedLocales; + } + + public void setSupportedLocales(Set supportedLocales) { + this.supportedLocales = supportedLocales; + } + + public String getDefaultLocale() { + return defaultLocale; + } + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 9bee5a52c0..655a45293c 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -1085,5 +1085,40 @@ public class RealmAdapter extends AbstractMongoAdapter impleme return getId().hashCode(); } + @Override + public boolean isInternationalizationEnabled() { + return realm.isInternationalizationEnabled(); + } + @Override + public void setInternationalizationEnabled(boolean enabled) { + realm.setInternationalizationEnabled(enabled); + updateRealm(); + } + + @Override + public Set getSupportedLocales() { + return new HashSet(realm.getSupportedLocales()); + } + + @Override + public void setSupportedLocales(Set locales) { + if (locales != null) { + realm.setEventsListeners(new ArrayList(locales)); + } else { + realm.setEventsListeners(Collections.EMPTY_LIST); + } + updateRealm(); + } + + @Override + public String getDefaultLocale() { + return realm.getDefaultLocale(); + } + + @Override + public void setDefaultLocale(String locale) { + realm.setDefaultLocale(locale); + updateRealm(); + } } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 183ebd571c..0e4b089385 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -22,6 +22,7 @@ import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper; import org.keycloak.protocol.saml.mappers.SAMLRoleListMapper; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; +import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.admin.ClientAttributeCertificateResource; import org.keycloak.services.resources.flows.Flows; @@ -94,6 +95,7 @@ public class SamlProtocol implements LoginProtocol { protected UriInfo uriInfo; + protected HttpHeaders headers; @Override @@ -114,6 +116,12 @@ public class SamlProtocol implements LoginProtocol { return this; } + @Override + public SamlProtocol setHttpHeaders(HttpHeaders headers){ + this.headers = headers; + return this; + } + @Override public Response cancelLogin(ClientSessionModel clientSession) { return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()); @@ -141,7 +149,7 @@ public class SamlProtocol implements LoginProtocol { return builder.redirectBinding().response(); } } catch (Exception e) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.FAILED_TO_PROCESS_RESPONSE ); } } @@ -295,7 +303,7 @@ public class SamlProtocol implements LoginProtocol { samlDocument = builder.buildDocument(samlModel); } catch (Exception e) { logger.error("failed", e); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo,headers, Messages.FAILED_TO_PROCESS_RESPONSE); } SAML2BindingBuilder2 bindingBuilder = new SAML2BindingBuilder2(); @@ -317,7 +325,7 @@ public class SamlProtocol implements LoginProtocol { publicKey = SamlProtocolUtils.getEncryptionValidationKey(client); } catch (Exception e) { logger.error("failed", e); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.FAILED_TO_PROCESS_RESPONSE); } bindingBuilder.encrypt(publicKey); } @@ -329,7 +337,7 @@ public class SamlProtocol implements LoginProtocol { } } catch (Exception e) { logger.error("failed", e); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.FAILED_TO_PROCESS_RESPONSE ); } } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index 80420318ba..3e7cd2f300 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -23,6 +23,7 @@ import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.HttpAuthenticationManager; +import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.flows.Flows; import org.keycloak.util.StreamUtil; @@ -106,18 +107,18 @@ public class SamlService { if (!checkSsl()) { event.event(EventType.LOGIN); event.error(Errors.SSL_REQUIRED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED ); } if (!realm.isEnabled()) { event.event(EventType.LOGIN_ERROR); event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED); } if (samlRequest == null && samlResponse == null) { event.event(EventType.LOGIN); event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST ); } return null; @@ -131,7 +132,7 @@ public class SamlService { if (!uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) { event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE); event.detail(Details.REASON, "invalid_destination"); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid request."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST); } AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false); @@ -139,7 +140,7 @@ public class SamlService { logger.warn("Unknown saml response."); event.event(EventType.LOGOUT); event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST); } // assume this is a logout response UserSessionModel userSession = authResult.getSession(); @@ -148,10 +149,10 @@ public class SamlService { logger.warn("UserSession is not tagged as logging out."); event.event(EventType.LOGOUT); event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST); } logger.debug("logout response"); - Response response = authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection); + Response response = authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers); event.success(); return response; } @@ -161,7 +162,7 @@ public class SamlService { if (documentHolder == null) { event.event(EventType.LOGIN); event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST); } SAML2Object samlObject = documentHolder.getSamlObject(); @@ -173,23 +174,23 @@ public class SamlService { if (client == null) { event.event(EventType.LOGIN); event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNKNOWN_LOGIN_REQUESTER); } if (!client.isEnabled()) { event.event(EventType.LOGIN); event.error(Errors.CLIENT_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.LOGIN_REQUESTER_NOT_ENABLED); } if ((client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { event.event(EventType.LOGIN); event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.BEARER_ONLY); } if (client.isDirectGrantsOnly()) { event.event(EventType.LOGIN); event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.DIRECT_GRANTS_ONLY ); } try { @@ -198,7 +199,7 @@ public class SamlService { SamlService.logger.error("request validation failed", e); event.event(EventType.LOGIN); event.error(Errors.INVALID_SIGNATURE); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid requester."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUESTER); } logger.debug("verified request"); if (samlObject instanceof AuthnRequestType) { @@ -216,7 +217,7 @@ public class SamlService { } else { event.event(EventType.LOGIN); event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST); } } @@ -230,7 +231,7 @@ public class SamlService { if (!uriInfo.getAbsolutePath().equals(requestAbstractType.getDestination())) { event.error(Errors.INVALID_SAML_AUTHN_REQUEST); event.detail(Details.REASON, "invalid_destination"); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid request."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST); } String bindingType = getBindingType(requestAbstractType); if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING))) bindingType = SamlProtocol.SAML_POST_BINDING; @@ -252,7 +253,7 @@ public class SamlService { if (redirect == null) { event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REDIRECT_URI ); } @@ -275,7 +276,7 @@ public class SamlService { } else { event.error(Errors.INVALID_SAML_AUTHN_REQUEST); event.detail(Details.REASON, "unsupported_nameid_format"); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unsupported NameIDFormat."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNSUPPORTED_NAME_ID_FORMAT); } } @@ -284,10 +285,10 @@ public class SamlService { // SPNEGO/Kerberos authentication TODO: This should be somehow pluggable instead of hardcoded this way (Authentication interceptors?) HttpAuthenticationManager httpAuthManager = new HttpAuthenticationManager(session, clientSession, realm, uriInfo, request, clientConnection, event); - HttpAuthenticationManager.HttpAuthOutput httpAuthOutput = httpAuthManager.spnegoAuthenticate(); + HttpAuthenticationManager.HttpAuthOutput httpAuthOutput = httpAuthManager.spnegoAuthenticate(headers); if (httpAuthOutput.getResponse() != null) return httpAuthOutput.getResponse(); - LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) + LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo, headers) .setClientSessionCode(new ClientSessionCode(realm, clientSession).getCode()); // Attach state from SPNEGO authentication @@ -339,7 +340,7 @@ public class SamlService { if (!uriInfo.getAbsolutePath().equals(logoutRequest.getDestination())) { event.error(Errors.INVALID_SAML_LOGOUT_REQUEST); event.detail(Details.REASON, "invalid_destination"); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid request."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST); } // authenticate identity cookie, but ignore an access token timeout as we're logging out anyways. @@ -366,7 +367,7 @@ public class SamlService { } } logger.debug("browser Logout"); - return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection); + return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers); } @@ -379,7 +380,7 @@ public class SamlService { if (redirectUri != null) { redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client); if (redirectUri == null) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REDIRECT_URI ); } } if (redirectUri != null) { @@ -391,7 +392,7 @@ public class SamlService { } private Response logout(UserSessionModel userSession) { - Response response = authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection); + Response response = authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers); if (response == null) event.user(userSession.getUser()).session(userSession).success(); return response; } diff --git a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java index 7bd4d033c8..0290771d7a 100755 --- a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java @@ -7,6 +7,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.provider.Provider; import org.keycloak.services.managers.ClientSessionCode; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -21,6 +22,8 @@ public interface LoginProtocol extends Provider { LoginProtocol setUriInfo(UriInfo uriInfo); + LoginProtocol setHttpHeaders(HttpHeaders headers); + Response cancelLogin(ClientSessionModel clientSession); Response invalidSessionError(ClientSessionModel clientSession); Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 3900f1aaf3..433acd7846 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -33,6 +33,7 @@ import org.keycloak.protocol.LoginProtocol; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; @@ -63,13 +64,17 @@ public class OIDCLoginProtocol implements LoginProtocol { protected UriInfo uriInfo; - public OIDCLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo) { + protected HttpHeaders headers; + + public OIDCLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { this.session = session; this.realm = realm; this.uriInfo = uriInfo; + this.headers = headers; } - public OIDCLoginProtocol() { + public OIDCLoginProtocol(){ + } @Override @@ -90,6 +95,12 @@ public class OIDCLoginProtocol implements LoginProtocol { return this; } + @Override + public OIDCLoginProtocol setHttpHeaders(HttpHeaders headers){ + this.headers = headers; + return this; + } + @Override public Response cancelLogin(ClientSessionModel clientSession) { String redirect = clientSession.getRedirectUri(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java index c2ec74bb4b..fd4b3b154d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolService.java @@ -67,6 +67,9 @@ public class OIDCLoginProtocolService { @Context private KeycloakSession session; + @Context + private HttpHeaders headers; + public OIDCLoginProtocolService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { this.realm = realm; this.tokenManager = new TokenManager(); @@ -226,7 +229,7 @@ public class OIDCLoginProtocolService { @Path("oauth/oob") @GET public Response installedAppUrnCallback(final @QueryParam("code") String code, final @QueryParam("error") String error, final @QueryParam("error_description") String errorDescription) { - LoginFormsProvider forms = Flows.forms(session, realm, null, uriInfo); + LoginFormsProvider forms = Flows.forms(session, realm, null, uriInfo, headers); if (code != null) { return forms.setClientSessionCode(code).createCode(); } else { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 07fd828c59..da9fdd83fa 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -30,6 +30,7 @@ import org.keycloak.representations.RefreshToken; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.util.Time; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.util.HashSet; @@ -73,7 +74,7 @@ public class TokenManager { } } - public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, AccessToken oldToken) throws OAuthErrorException { + public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, AccessToken oldToken, HttpHeaders headers) throws OAuthErrorException { UserModel user = session.users().getUserById(oldToken.getSubject(), realm); if (user == null) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user"); @@ -85,7 +86,7 @@ public class TokenManager { UserSessionModel userSession = session.sessions().getUserSession(realm, oldToken.getSessionState()); if (!AuthenticationManager.isSessionValid(realm, userSession)) { - AuthenticationManager.logout(session, realm, userSession, uriInfo, connection); + AuthenticationManager.logout(session, realm, userSession, uriInfo, connection, headers); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active"); } ClientSessionModel clientSession = null; @@ -124,12 +125,12 @@ public class TokenManager { } - public AccessTokenResponse refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event) throws OAuthErrorException { + public AccessTokenResponse refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException { RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken); event.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()); - TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken); + TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken, headers); // validate authorizedClient is same as validated client if (!validation.clientSession.getClient().getId().equals(authorizedClient.getId())) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token. Token client and authorized client don't match"); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 6f6441f55e..708fb2119b 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -24,6 +24,7 @@ import org.keycloak.services.ErrorPageException; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.HttpAuthenticationManager; +import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Urls; @@ -116,7 +117,7 @@ public class AuthorizationEndpoint { action = Action.REGISTER; if (!realm.isRegistrationAllowed()) { - throw new ErrorPageException(session, realm, uriInfo, "Registration not allowed"); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.REGISTRATION_NOT_ALLOWED); } return this; @@ -148,21 +149,21 @@ public class AuthorizationEndpoint { private void checkSsl() { if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { event.error(Errors.SSL_REQUIRED); - throw new ErrorPageException(session, realm, uriInfo, "HTTPS required"); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED); } } private void checkRealm() { if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); - throw new ErrorPageException(session, realm, uriInfo, "Realm not enabled"); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED); } } private void checkClient() { if (clientId == null) { event.error(Errors.INVALID_REQUEST); - throw new ErrorPageException(session, realm, uriInfo, "Missing paramater: " + OIDCLoginProtocol.CLIENT_ID_PARAM); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM ); } event.client(clientId); @@ -170,17 +171,17 @@ public class AuthorizationEndpoint { client = realm.findClient(clientId); if (client == null) { event.error(Errors.CLIENT_NOT_FOUND); - throw new ErrorPageException(session, realm, uriInfo, "Client not found"); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.CLIENT_NOT_FOUND ); } if ((client instanceof ApplicationModel) && ((ApplicationModel) client).isBearerOnly()) { event.error(Errors.NOT_ALLOWED); - throw new ErrorPageException(session, realm, uriInfo, "Bearer only clients are not allowed to initiate browser login"); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.BEARER_ONLY ); } if (client.isDirectGrantsOnly()) { event.error(Errors.NOT_ALLOWED); - throw new ErrorPageException(session, realm, uriInfo, "Direct grants only clients are not allowed to initiate browser login"); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.DIRECT_GRANTS_ONLY); } } @@ -190,7 +191,7 @@ public class AuthorizationEndpoint { responseType = legacyResponseType; } else { event.error(Errors.INVALID_REQUEST); - throw new ErrorPageException(session, realm, uriInfo, "Missing query parameter: " + OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.MISSING_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM ); } } @@ -200,7 +201,7 @@ public class AuthorizationEndpoint { action = Action.CODE; } else { event.error(Errors.INVALID_REQUEST); - throw new ErrorPageException(session, realm, uriInfo, "Invalid " + OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.INVALID_PARAMETER, OIDCLoginProtocol.RESPONSE_TYPE_PARAM ); } } @@ -210,7 +211,7 @@ public class AuthorizationEndpoint { redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUriParam, realm, client); if (redirectUri == null) { event.error(Errors.INVALID_REDIRECT_URI); - throw new ErrorPageException(session, realm, uriInfo, "Invalid " + OIDCLoginProtocol.REDIRECT_URI_PARAM); + throw new ErrorPageException(session, realm, uriInfo, headers, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM); } } @@ -237,8 +238,8 @@ public class AuthorizationEndpoint { IdentityProviderModel identityProviderModel = realm.getIdentityProviderById(idpHint); if (identityProviderModel == null) { - return Flows.forms(session, realm, null, uriInfo) - .setError("Could not find an identity provider with the identifier [" + idpHint + "].") + return Flows.forms(session, realm, null, uriInfo, headers) + .setError(Messages.IDENTITY_PROVIDER_NOT_FOUND, idpHint) .createErrorPage(); } return buildRedirectToIdentityProvider(idpHint, accessCode); @@ -249,11 +250,11 @@ public class AuthorizationEndpoint { // SPNEGO/Kerberos authentication TODO: This should be somehow pluggable instead of hardcoded this way (Authentication interceptors?) HttpAuthenticationManager httpAuthManager = new HttpAuthenticationManager(session, clientSession, realm, uriInfo, request, clientConnection, event); - HttpAuthenticationManager.HttpAuthOutput httpAuthOutput = httpAuthManager.spnegoAuthenticate(); + HttpAuthenticationManager.HttpAuthOutput httpAuthOutput = httpAuthManager.spnegoAuthenticate(headers); if (httpAuthOutput.getResponse() != null) return httpAuthOutput.getResponse(); if (prompt != null && prompt.equals("none")) { - OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo); + OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers); return oauth.cancelLogin(clientSession); } @@ -271,13 +272,13 @@ public class AuthorizationEndpoint { return buildRedirectToIdentityProvider(identityProviders.get(0).getId(), accessCode); } - return Flows.forms(session, realm, null, uriInfo).setError("Realm [" + realm.getName() + "] supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo, headers).setError(Messages.IDENTITY_PROVIDER_NOT_UNIQUE, realm.getName()).createErrorPage(); } - return Flows.forms(session, realm, null, uriInfo).setError("Realm [" + realm.getName() + "] does not support any credential type.").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo, headers).setError(Messages.REALM_SUPPORTS_NO_CREDENTIALS, realm.getName()).createErrorPage(); } - LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) + LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo, headers) .setClientSessionCode(accessCode); // Attach state from SPNEGO authentication @@ -306,7 +307,7 @@ public class AuthorizationEndpoint { private Response buildRegister() { authManager.expireIdentityCookie(realm, uriInfo, clientConnection); - return Flows.forms(session, realm, client, uriInfo) + return Flows.forms(session, realm, client, uriInfo, headers) .setClientSessionCode(new ClientSessionCode(realm, clientSession).getCode()) .createRegistration(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 16ab80c043..a47aa6a2c1 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -21,6 +21,7 @@ import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.representations.RefreshToken; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.flows.Flows; @@ -91,7 +92,7 @@ public class LogoutEndpoint { if (redirectUri != null) { String validatedRedirect = RedirectUtils.verifyRealmRedirectUri(uriInfo, redirectUri, realm); if (validatedRedirect == null) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REDIRECT_URI); } return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build(); } else { @@ -143,7 +144,7 @@ public class LogoutEndpoint { } private void logout(UserSessionModel userSession) { - authManager.logout(session, realm, userSession, uriInfo, clientConnection); + authManager.logout(session, realm, userSession, uriInfo, clientConnection, headers); event.user(userSession.getUser()).session(userSession).success(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index ab78e6c7a9..730e9be9a6 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -276,7 +276,7 @@ public class TokenEndpoint { AccessTokenResponse res; try { - res = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event); + res = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event, headers); } catch (OAuthErrorException e) { event.error(Errors.INVALID_TOKEN); throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java index caef4361e0..8dad6fd490 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/ValidateTokenEndpoint.java @@ -20,10 +20,7 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; +import javax.ws.rs.core.*; import java.util.HashMap; import java.util.Map; @@ -43,6 +40,9 @@ public class ValidateTokenEndpoint { @Context private UriInfo uriInfo; + @Context + private HttpHeaders headers; + private TokenManager tokenManager; private RealmModel realm; private EventBuilder event; @@ -81,7 +81,7 @@ public class ValidateTokenEndpoint { event.user(token.getSubject()).session(token.getSessionState()).detail(Details.VALIDATE_ACCESS_TOKEN, token.getId()); try { - tokenManager.validateToken(session, uriInfo, clientConnection, realm, token); + tokenManager.validateToken(session, uriInfo, clientConnection, realm, token, headers); } catch (OAuthErrorException e) { Map error = new HashMap(); error.put(OAuth2Constants.ERROR, e.getError()); diff --git a/services/src/main/java/org/keycloak/services/ErrorPageException.java b/services/src/main/java/org/keycloak/services/ErrorPageException.java index 3f8d435ff0..634ea7a7ac 100644 --- a/services/src/main/java/org/keycloak/services/ErrorPageException.java +++ b/services/src/main/java/org/keycloak/services/ErrorPageException.java @@ -5,6 +5,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.services.resources.flows.Flows; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -16,18 +17,22 @@ public class ErrorPageException extends WebApplicationException { private final KeycloakSession session; private final RealmModel realm; private final UriInfo uriInfo; + private final HttpHeaders httpHeaders; private final String errorMessage; + private final Object[] parameters; - public ErrorPageException(KeycloakSession session, RealmModel realm, UriInfo uriInfo, String errorMessage) { + public ErrorPageException(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String errorMessage, Object ... parameters) { this.session = session; this.realm = realm; this.uriInfo = uriInfo; + this.httpHeaders = headers; this.errorMessage = errorMessage; + this.parameters = parameters; } @Override public Response getResponse() { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, errorMessage); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, httpHeaders, errorMessage, parameters); } } diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java index e3bb56971a..9e0e4ab161 100755 --- a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java @@ -42,7 +42,7 @@ public class AppAuthManager extends AuthenticationManager { public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { String tokenString = extractAuthorizationHeaderToken(headers); if (tokenString == null) return null; - AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, true, tokenString); + AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, true, tokenString, headers); return authResult; } 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 aa27f17a94..f37b83781d 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -79,7 +79,7 @@ public class AuthenticationManager { return userSession != null && userSession.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() > currentTime && max > currentTime; } - public static void logout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection) { + public static void logout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { if (userSession == null) return; UserModel user = userSession.getUser(); userSession.setState(UserSessionModel.State.LOGGING_OUT); @@ -95,6 +95,7 @@ public class AuthenticationManager { if (authMethod == null) continue; // must be a keycloak service like account LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod); protocol.setRealm(realm) + .setHttpHeaders(headers) .setUriInfo(uriInfo); protocol.backchannelLogout(userSession, clientSession); clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT); @@ -105,7 +106,7 @@ public class AuthenticationManager { } - public static Response browserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection) { + public static Response browserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { if (userSession == null) return null; UserModel user = userSession.getUser(); @@ -128,6 +129,7 @@ public class AuthenticationManager { if (authMethod == null) continue; // must be a keycloak service like account LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod); protocol.setRealm(realm) + .setHttpHeaders(headers) .setUriInfo(uriInfo); try { logger.debugv("backchannel logout to: {0}", client.getClientId()); @@ -140,12 +142,13 @@ public class AuthenticationManager { } if (redirectClients.size() == 0) { - return finishBrowserLogout(session, realm, userSession, uriInfo, connection); + return finishBrowserLogout(session, realm, userSession, uriInfo, connection, headers); } for (ClientSessionModel nextRedirectClient : redirectClients) { String authMethod = nextRedirectClient.getAuthMethod(); LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod); protocol.setRealm(realm) + .setHttpHeaders(headers) .setUriInfo(uriInfo); // setting this to logged out cuz I"m not sure protocols can always verify that the client was logged out or not nextRedirectClient.setAction(ClientSessionModel.Action.LOGGED_OUT); @@ -161,16 +164,17 @@ public class AuthenticationManager { } } - return finishBrowserLogout(session, realm, userSession, uriInfo, connection); + return finishBrowserLogout(session, realm, userSession, uriInfo, connection, headers); } - protected static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection) { + protected static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { expireIdentityCookie(realm, uriInfo, connection); expireRememberMeCookie(realm, uriInfo, connection); userSession.setState(UserSessionModel.State.LOGGED_OUT); String method = userSession.getNote(KEYCLOAK_LOGOUT_PROTOCOL); LoginProtocol protocol = session.getProvider(LoginProtocol.class, method); protocol.setRealm(realm) + .setHttpHeaders(headers) .setUriInfo(uriInfo); Response response = protocol.finishLogout(userSession); session.sessions().removeUserSession(realm, userSession); @@ -285,7 +289,7 @@ public class AuthenticationManager { } String tokenString = cookie.getValue(); - AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, checkActive, tokenString); + AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, checkActive, tokenString, headers); if (authResult == null) { expireIdentityCookie(realm, uriInfo, connection); return null; @@ -335,6 +339,7 @@ public class AuthenticationManager { if (userSession.isRememberMe()) createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection); LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); protocol.setRealm(realm) + .setHttpHeaders(request.getHttpHeaders()) .setUriInfo(uriInfo); return protocol.authenticated(userSession, new ClientSessionCode(realm, clientSession)); @@ -364,7 +369,7 @@ public class AuthenticationManager { UserModel.RequiredAction action = user.getRequiredActions().iterator().next(); accessCode.setRequiredAction(action); - LoginFormsProvider loginFormsProvider = Flows.forms(session, realm, client, uriInfo).setClientSessionCode(accessCode.getCode()).setUser(user); + LoginFormsProvider loginFormsProvider = Flows.forms(session, realm, client, uriInfo, request.getHttpHeaders()).setClientSessionCode(accessCode.getCode()).setUser(user); if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL)) { event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); LoginActionsService.createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); @@ -387,7 +392,7 @@ public class AuthenticationManager { } } - return Flows.forms(session, realm, client, uriInfo) + return Flows.forms(session, realm, client, uriInfo, request.getHttpHeaders()) .setClientSessionCode(accessCode.getCode()) .setAccessRequest(realmRoles, resourceRoles) .setClient(client) @@ -415,7 +420,7 @@ public class AuthenticationManager { } } - protected AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, String tokenString) { + protected AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, String tokenString, HttpHeaders headers) { try { AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName(), checkActive); if (checkActive) { @@ -435,7 +440,7 @@ public class AuthenticationManager { UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState()); if (!isSessionValid(realm, userSession)) { - if (userSession != null) logout(session, realm, userSession, uriInfo, connection); + if (userSession != null) logout(session, realm, userSession, uriInfo, connection, headers); logger.debug("User session not active"); return null; } diff --git a/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java index 7dca9c1482..702b684aba 100644 --- a/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java @@ -58,7 +58,7 @@ public class HttpAuthenticationManager { } - public HttpAuthOutput spnegoAuthenticate() { + public HttpAuthOutput spnegoAuthenticate(HttpHeaders headers) { boolean kerberosSupported = false; for (RequiredCredentialModel c : realm.getRequiredCredentials()) { if (c.getType().equals(CredentialRepresentation.KERBEROS)) { @@ -96,7 +96,7 @@ public class HttpAuthenticationManager { CredentialValidationOutput output = session.users().validCredentials(realm, spnegoCredential); if (output.getAuthStatus() == CredentialValidationOutput.Status.AUTHENTICATED) { - return sendResponse(output.getAuthenticatedUser(), output.getState(), "spnego"); + return sendResponse(output.getAuthenticatedUser(), output.getState(), "spnego", headers); } else { String spnegoResponseToken = (String) output.getState().get(KerberosConstants.RESPONSE_TOKEN); return challengeNegotiation(spnegoResponseToken); @@ -106,7 +106,7 @@ public class HttpAuthenticationManager { // Send response after successful authentication - private HttpAuthOutput sendResponse(UserModel user, Map authState, String authMethod) { + private HttpAuthOutput sendResponse(UserModel user, Map authState, String authMethod, HttpHeaders headers) { if (logger.isTraceEnabled()) { logger.trace("User " + user.getUsername() + " authenticated with " + authMethod); } @@ -114,7 +114,7 @@ public class HttpAuthenticationManager { Response response; if (!user.isEnabled()) { event.error(Errors.USER_DISABLED); - response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, Messages.ACCOUNT_DISABLED); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.ACCOUNT_DISABLED); } else { UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), authMethod, false); diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index 0ad03522f8..a1cc072e9b 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -26,67 +26,151 @@ package org.keycloak.services.messages; */ public class Messages { - public static final String ACCOUNT_DISABLED = "accountDisabled"; - public static final String ACCOUNT_TEMPORARILY_DISABLED = "accountTemporarilyDisabled"; + public static final String INVALID_USER = "invalidUserMessage"; - public static final String INVALID_PASSWORD = "invalidPassword"; + public static final String INVALID_EMAIL = "invalidEmailMessage"; - public static final String INVALID_PASSWORD_EXISTING = "invalidPasswordExisting"; + public static final String ACCOUNT_DISABLED = "accountDisabledMessage"; - public static final String INVALID_PASSWORD_CONFIRM = "invalidPasswordConfirm"; + public static final String ACCOUNT_TEMPORARILY_DISABLED = "accountTemporarilyDisabledMessage"; - public static final String INVALID_EMAIL = "invalidEmail"; + public static final String EXPIRED_CODE = "expiredCodeMessage"; - public static final String INVALID_USER = "invalidUser"; + public static final String MISSING_FIRST_NAME = "missingFirstNameMessage"; - public static final String EXPIRED_CODE = "expiredCode"; + public static final String MISSING_LAST_NAME = "missingLastNameMessage"; - public static final String READ_ONLY_USER = "readOnlyUser"; + public static final String MISSING_EMAIL = "missingEmailMessage"; - public static final String READ_ONLY_PASSWORD = "readOnlyPassword"; + public static final String MISSING_USERNAME = "missingUsernameMessage"; - public static final String MISSING_EMAIL = "missingEmail"; + public static final String MISSING_PASSWORD = "missingPasswordMessage"; - public static final String MISSING_FIRST_NAME = "missingFirstName"; + public static final String MISSING_TOTP = "missingTotpMessage"; - public static final String MISSING_LAST_NAME = "missingLastName"; + public static final String NOTMATCH_PASSWORD = "notMatchPasswordMessage"; - public static final String MISSING_PASSWORD = "missingPassword"; + public static final String INVALID_PASSWORD_EXISTING = "invalidPasswordExistingMessage"; - public static final String NOTMATCH_PASSWORD = "notMatchPassword"; + public static final String INVALID_PASSWORD_CONFIRM = "invalidPasswordConfirmMessage"; - public static final String MISSING_USERNAME = "missingUsername"; + public static final String INVALID_TOTP = "invalidTotpMessage"; - public static final String MISSING_TOTP = "missingTotp"; + public static final String USERNAME_EXISTS = "usernameExistsMessage"; - public static final String INVALID_TOTP = "invalidTotp"; + public static final String EMAIL_EXISTS = "emailExistsMessage"; - public static final String USERNAME_EXISTS = "usernameExists"; + public static final String FEDERATED_IDENTITY_EMAIL_EXISTS = "federatedIdentityEmailExistsMessage"; - public static final String EMAIL_EXISTS = "emailExists"; + public static final String FEDERATED_IDENTITY_USERNAME_EXISTS = "federatedIdentityUsernameExistsMessage"; - public static final String ACTION_WARN_TOTP = "actionTotpWarning"; + public static final String CONFIGURE_TOTP = "configureTotpMessage"; - public static final String ACTION_WARN_PROFILE = "actionProfileWarning"; + public static final String UPDATE_PROFILE = "updateProfileMessage"; - public static final String ACTION_WARN_PASSWD = "actionPasswordWarning"; + public static final String UPDATE_PASSWORD = "updatePasswordMessage"; - public static final String ACTION_WARN_EMAIL = "actionEmailWarning"; + public static final String VERIFY_EMAIL = "verifyEmailMessage"; - public static final String MISSING_IDENTITY_PROVIDER = "missingIdentityProvider"; + public static final String EMAIL_VERIFIED = "emailVerifiedMessage"; - public static final String INVALID_FEDERATED_IDENTITY_ACTION = "invalidFederatedIdentityAction"; + public static final String EMAIL_SENT = "emailSentMessage"; - public static final String IDENTITY_PROVIDER_NOT_FOUND = "identityProviderNotFound"; + public static final String EMAIL_SENT_ERROR = "emailSendErrorMessage"; - public static final String FEDERATED_IDENTITY_NOT_ACTIVE = "federatedIdentityLinkNotActive"; + public static final String ACCOUNT_UPDATED = "accountUpdatedMessage"; - public static final String FEDERATED_IDENTITY_REMOVING_LAST_PROVIDER = "federatedIdentityRemovingLastProvider"; + public static final String ACCOUNT_PASSWORD_UPDATED = "accountPasswordUpdatedMessage"; - public static final String IDENTITY_PROVIDER_REDIRECT_ERROR = "identityProviderRedirectError"; + public static final String NO_ACCESS = "noAccessMessage"; - public static final String IDENTITY_PROVIDER_REMOVED = "identityProviderRemoved"; + public static final String FAILED_TO_PROCESS_RESPONSE = "failedToProcessResponseMessage"; - public static final String ERROR = "error"; + public static final String HTTPS_REQUIRED = "httpsRequiredMessage"; + public static final String REALM_NOT_ENABLED = "realmNotEnabledMessage"; + + public static final String INVALID_REQUEST = "invalidRequestMessage"; + + public static final String INVALID_REQUESTER = "invalidRequesterMessage"; + + public static final String UNKNOWN_LOGIN_REQUESTER = "unknownLoginRequesterMessage"; + + public static final String LOGIN_REQUESTER_NOT_ENABLED = "loginRequesterNotEnabledMessage"; + + public static final String BEARER_ONLY = "bearerOnlyMessage"; + + public static final String DIRECT_GRANTS_ONLY = "directGrantsOnlyMessage"; + + public static final String INVALID_REDIRECT_URI = "invalidRedirectUriMessage"; + + public static final String UNSUPPORTED_NAME_ID_FORMAT = "unsupportedNameIdFormatMessage"; + + public static final String REGISTRATION_NOT_ALLOWED = "registrationNotAllowedMessage"; + + public static final String PERMISSION_NOT_APPROVED = "permissionNotApprovedMessage"; + + public static final String NO_RELAY_STATE_IN_RESPONSE = "noRelayStateInResponseMessage"; + + public static final String IDENTITY_PROVIDER_ALREADY_LINKED = "identityProviderAlreadyLinkedMessage"; + + public static final String INSUFFICIENT_PERMISSION = "insufficientPermissionMessage"; + + public static final String COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST = "couldNotProceedWithAuthenticationRequestMessage"; + + public static final String COULD_NOT_OBTAIN_TOKEN = "couldNotObtainTokenMessage"; + + public static final String UNEXPECTED_ERROR_RETRIEVING_TOKEN = "unexpectedErrorRetrievingTokenMessage"; + + public static final String IDENTITY_PROVIDER_AUTHENTICATION_FAILED = "identityProviderAuthenticationFailedMessage"; + + public static final String UNEXPECTED_ERROR_HANDLING_RESPONSE = "unexpectedErrorHandlingResponseMessage"; + + public static final String COULD_NOT_SEND_AUTHENTICATION_REQUEST = "couldNotSendAuthenticationRequestMessage"; + + public static final String UNEXPECTED_ERROR_HANDLING_REQUEST = "unexpectedErrorHandlingRequestMessage"; + + public static final String INVALID_ACCESS_CODE = "invalidAccessCodeMessage"; + + public static final String SESSION_NOT_ACTIVE = "sessionNotActiveMessage"; + + public static final String UNKNOWN_CODE = "unknownCodeMessage"; + + public static final String INVALID_CODE = "invalidCodeMessage"; + + public static final String IDENTITY_PROVIDER_UNEXPECTED_ERROR = "identityProviderUnexpectedErrorMessage"; + + public static final String IDENTITY_PROVIDER_NOT_FOUND = "identityProviderNotFoundMessage"; + + public static final String IDENTITY_PROVIDER_NOT_UNIQUE = "identityProviderNotUniqueMessage"; + + public static final String REALM_SUPPORTS_NO_CREDENTIALS = "realmSupportsNoCredentialsMessage"; + + public static final String READ_ONLY_USER = "readOnlyUserMessage"; + + public static final String READ_ONLY_PASSWORD = "readOnlyPasswordMessage"; + + public static final String SUCCESS_TOTP_REMOVED = "successTotpRemovedMessage"; + + public static final String SUCCESS_TOTP = "successTotpMessage"; + + public static final String MISSING_IDENTITY_PROVIDER = "missingIdentityProviderMessage"; + + public static final String INVALID_FEDERATED_IDENTITY_ACTION = "invalidFederatedIdentityActionMessage"; + + public static final String FEDERATED_IDENTITY_NOT_ACTIVE = "federatedIdentityLinkNotActiveMessage"; + + public static final String FEDERATED_IDENTITY_REMOVING_LAST_PROVIDER = "federatedIdentityRemovingLastProviderMessage"; + + public static final String IDENTITY_PROVIDER_REDIRECT_ERROR = "identityProviderRedirectErrorMessage"; + + public static final String IDENTITY_PROVIDER_REMOVED = "identityProviderRemovedMessage"; + + public static final String MISSING_PARAMETER = "missingParameterMessage"; + + public static final String CLIENT_NOT_FOUND = "clientNotFoundMessage"; + + public static final String INVALID_PARAMETER = "invalidParameterMessage"; + + public static final String FEDERATED_IDENTITY_REGISTRATION_EMAIL_MISSING = "federatedIdentityRegistrationEmailMissingMessage"; } diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 8b5297b298..1efc33a032 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -32,20 +32,7 @@ import org.keycloak.events.Event; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventType; -import org.keycloak.models.AccountRoles; -import org.keycloak.models.ApplicationModel; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.Constants; -import org.keycloak.models.FederatedIdentityModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelReadOnlyException; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserCredentialValueModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; +import org.keycloak.models.*; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -160,7 +147,7 @@ public class AccountService { public void init() { eventStore = session.getProvider(EventStoreProvider.class); - account = session.getProvider(AccountProvider.class).setRealm(realm).setUriInfo(uriInfo); + account = session.getProvider(AccountProvider.class).setRealm(realm).setUriInfo(uriInfo).setHttpHeaders(headers); AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(session, realm, uriInfo, clientConnection, headers); if (authResult != null) { @@ -242,7 +229,7 @@ public class AccountService { try { require(AccountRoles.MANAGE_ACCOUNT); } catch (ForbiddenException e) { - return Flows.forms(session, realm, null, uriInfo).setError("No access").createErrorPage(); + return Flows.forms(session, realm, null, uriInfo, headers).setError(Messages.NO_ACCESS).createErrorPage(); } setReferrerOnPage(); @@ -442,7 +429,7 @@ public class AccountService { event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); } setReferrerOnPage(); - return account.setSuccess("accountUpdated").createResponse(AccountPages.ACCOUNT); + return account.setSuccess(Messages.ACCOUNT_UPDATED).createResponse(AccountPages.ACCOUNT); } catch (ModelReadOnlyException roe) { setReferrerOnPage(); return account.setError(Messages.READ_ONLY_USER).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT); @@ -466,7 +453,7 @@ public class AccountService { event.event(EventType.REMOVE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); setReferrerOnPage(); - return account.setSuccess("successTotpRemoved").createResponse(AccountPages.TOTP); + return account.setSuccess(Messages.SUCCESS_TOTP_REMOVED).createResponse(AccountPages.TOTP); } @@ -545,7 +532,7 @@ public class AccountService { event.event(EventType.UPDATE_TOTP).client(auth.getClient()).user(auth.getUser()).success(); setReferrerOnPage(); - return account.setSuccess("successTotp").createResponse(AccountPages.TOTP); + return account.setSuccess(Messages.SUCCESS_TOTP).createResponse(AccountPages.TOTP); } /** @@ -614,7 +601,11 @@ public class AccountService { } catch (ModelReadOnlyException mre) { setReferrerOnPage(); return account.setError(Messages.READ_ONLY_PASSWORD).createResponse(AccountPages.PASSWORD); - } catch (Exception ape) { + }catch (ModelException me) { + logger.error("Failed to update password", me); + setReferrerOnPage(); + return account.setError(me.getMessage(), me.getParameters()).createResponse(AccountPages.PASSWORD); + }catch (Exception ape) { logger.error("Failed to update password", ape); setReferrerOnPage(); return account.setError(ape.getMessage()).createResponse(AccountPages.PASSWORD); @@ -631,7 +622,7 @@ public class AccountService { event.event(EventType.UPDATE_PASSWORD).client(auth.getClient()).user(auth.getUser()).success(); setReferrerOnPage(); - return account.setPasswordSet(true).setSuccess("accountPasswordUpdated").createResponse(AccountPages.PASSWORD); + return account.setPasswordSet(true).setSuccess(Messages.ACCOUNT_PASSWORD_UPDATED).createResponse(AccountPages.PASSWORD); } @Path("federated-identity-update") diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index c95b61807f..4666ff4afa 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -46,6 +46,7 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager.AuthResult; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.EventsManager; +import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Urls; import org.keycloak.social.SocialIdentityProvider; @@ -57,13 +58,8 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; +import javax.ws.rs.core.*; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -97,6 +93,10 @@ public class IdentityBrokerService { @Context private HttpRequest request; + + @Context + private HttpHeaders headers; + private EventBuilder event; public IdentityBrokerService(RealmModel realmModel) { @@ -135,12 +135,12 @@ public class IdentityBrokerService { return response; } } catch (IdentityBrokerException e) { - return redirectToErrorPage("Could not send authentication request to identity provider [" + providerId + "].", e); + return redirectToErrorPage(Messages.COULD_NOT_SEND_AUTHENTICATION_REQUEST, e, providerId); } catch (Exception e) { - return redirectToErrorPage("Unexpected error when handling authentication request to identity provider [" + providerId + "].", e); + return redirectToErrorPage(Messages.UNEXPECTED_ERROR_HANDLING_REQUEST, e, providerId); } - return redirectToErrorPage("Could not proceed with authentication request to identity provider."); + return redirectToErrorPage(Messages.COULD_NOT_PROCEED_WITH_AUTHENTICATION_REQUEST); } @GET @@ -191,11 +191,10 @@ public class IdentityBrokerService { } if (OAuthClientModel.class.isInstance(clientModel) && !forceRetrieval) { - return corsResponse(Flows.forms(this.session, this.realmModel, clientModel, this.uriInfo) + return corsResponse(Flows.forms(this.session, this.realmModel, clientModel, this.uriInfo, headers) .setClientSessionCode(authManager.extractAuthorizationHeaderToken(this.request.getHttpHeaders())) .setAccessRequest("Your information from " + providerId + " identity provider.") .setClient(clientModel) - .setUriInfo(this.uriInfo) .setActionUri(this.uriInfo.getRequestUri()) .createOAuthGrant(null), clientModel); } @@ -220,9 +219,9 @@ public class IdentityBrokerService { return badRequest("Invalid token."); } catch (IdentityBrokerException e) { - return redirectToErrorPage("Could not obtain token fron identity provider [" + providerId + "].", e); + return redirectToErrorPage(Messages.COULD_NOT_OBTAIN_TOKEN, e, providerId); } catch (Exception e) { - return redirectToErrorPage("Unexpected error when retrieving token from identity provider [" + providerId + "].", e); + return redirectToErrorPage(Messages.UNEXPECTED_ERROR_RETRIEVING_TOKEN, e, providerId); } } @@ -232,7 +231,7 @@ public class IdentityBrokerService { public Response consentTokenRetrieval(@PathParam("provider_id") String providerId, MultivaluedMap formData) { if (formData.containsKey("cancel")) { - return redirectToErrorPage("Permission not approved."); + return redirectToErrorPage(Messages.PERMISSION_NOT_APPROVED); } return getToken(providerId, true); @@ -251,7 +250,7 @@ public class IdentityBrokerService { String relayState = identityProvider.getRelayState(createAuthenticationRequest(providerId, null)); if (relayState == null) { - return redirectToErrorPage("No relay state in response from identity identity [" + providerId + "."); + return redirectToErrorPage(Messages.NO_RELAY_STATE_IN_RESPONSE, providerId); } if (isDebugEnabled()) { @@ -287,10 +286,10 @@ public class IdentityBrokerService { return performLocalAuthentication(identity, clientSessionCode); } catch (IdentityBrokerException e) { rollback(); - return redirectToErrorPage("Authentication failed. Could not authenticate with identity provider [" + providerId + "].", e); + return redirectToErrorPage(Messages.IDENTITY_PROVIDER_AUTHENTICATION_FAILED, e, providerId); } catch (Exception e) { rollback(); - return redirectToErrorPage("Unexpected error when handling response from identity provider [" + providerId + "].", e); + return redirectToErrorPage(Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE, e, providerId); } finally { if (this.session.getTransaction().isActive()) { this.session.getTransaction().commit(); @@ -353,7 +352,7 @@ public class IdentityBrokerService { this.event.event(EventType.IDENTITY_PROVIDER_ACCCOUNT_LINKING); if (federatedUser != null) { - return redirectToErrorPage("The identity returned by the identity provider [" + providerId + "] is already linked to other user."); + return redirectToErrorPage(Messages.IDENTITY_PROVIDER_ALREADY_LINKED, providerId); } UserModel authenticatedUser = clientSession.getUserSession().getUser(); @@ -364,12 +363,12 @@ public class IdentityBrokerService { if (!authenticatedUser.isEnabled()) { fireErrorEvent(Errors.USER_DISABLED); - return redirectToErrorPage("User is disabled."); + return redirectToErrorPage(Messages.ACCOUNT_DISABLED); } if (!authenticatedUser.hasRole(this.realmModel.getApplicationByName(ACCOUNT_MANAGEMENT_APP).getRole(MANAGE_ACCOUNT))) { fireErrorEvent(Errors.NOT_ALLOWED); - return redirectToErrorPage("Insufficient permissions to link identities."); + return redirectToErrorPage(Messages.INSUFFICIENT_PERMISSION); } this.session.users().addFederatedIdentity(this.realmModel, authenticatedUser, federatedIdentityModel); @@ -438,28 +437,28 @@ public class IdentityBrokerService { return Urls.identityProviderAuthnResponse(this.uriInfo.getBaseUri(), providerId, this.realmModel.getName()).toString(); } - private Response redirectToErrorPage(String message) { - return redirectToErrorPage(message, null); + private Response redirectToErrorPage(String message, Object ... parameters) { + return redirectToErrorPage(message, null, parameters); } - private Response redirectToErrorPage(String message, Throwable throwable) { + private Response redirectToErrorPage(String message, Throwable throwable, Object ... parameters) { if (message == null) { - message = "Unexpected error when authenticating with identity provider"; + message = Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR; } fireErrorEvent(message, throwable); - return Flows.forwardToSecurityFailurePage(this.session, this.realmModel, this.uriInfo, message); + return Flows.forwardToSecurityFailurePage(this.session, this.realmModel, this.uriInfo, headers, message, parameters); } private Response redirectToLoginPage(Throwable t, ClientSessionCode clientCode) { String message = t.getMessage(); if (message == null) { - message = "Unexpected error when authenticating with identity provider"; + message = Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR; } fireErrorEvent(message); - return Flows.forms(this.session, this.realmModel, clientCode.getClientSession().getClient(), this.uriInfo) + return Flows.forms(this.session, this.realmModel, clientCode.getClientSession().getClient(), this.uriInfo, headers) .setClientSessionCode(clientCode.getCode()) .setError(message) .createLogin(); @@ -535,7 +534,7 @@ public class IdentityBrokerService { if (existingUser != null) { fireErrorEvent(Errors.FEDERATED_IDENTITY_EMAIL_EXISTS); - throw new IdentityBrokerException("federatedIdentityEmailExists"); + throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_EMAIL_EXISTS); } String username = updatedIdentity.getUsername(); @@ -543,7 +542,7 @@ public class IdentityBrokerService { username = updatedIdentity.getEmail(); if (username == null || username.trim().length() == 0) { fireErrorEvent(Errors.FEDERATED_IDENTITY_REGISTRATION_EMAIL_MISSING); - throw new IdentityBrokerException("federatedIdentityRegistrationEmailMissing"); + throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_REGISTRATION_EMAIL_MISSING); // TODO KEYCLOAK-1053 (ask user to enter email address) should be implemented instead of plain exception as better solution for this case } username = username.trim(); @@ -555,7 +554,7 @@ public class IdentityBrokerService { if (existingUser != null) { fireErrorEvent(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS); - throw new IdentityBrokerException("federatedIdentityUsernameExists"); + throw new IdentityBrokerException(Messages.FEDERATED_IDENTITY_USERNAME_EXISTS); } if (isDebugEnabled()) { diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 0198644bd7..9e9282aa4a 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -32,15 +32,8 @@ 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.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; +import org.keycloak.models.*; import org.keycloak.models.UserModel.RequiredAction; -import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.LoginProtocol; @@ -160,7 +153,7 @@ public class LoginActionsService { return false; } else if (!clientCode.isValid(requiredAction)) { event.error(Errors.INVALID_CODE); - response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid code, please login again through your application."); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_CODE); return false; } else { return true; @@ -172,7 +165,7 @@ public class LoginActionsService { return false; } else if (!(clientCode.isValid(requiredAction) || clientCode.isValid(alternativeRequiredAction))) { event.error(Errors.INVALID_CODE); - response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid code, please login again through your application."); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo,headers, Messages.INVALID_CODE); return false; } else { return true; @@ -182,18 +175,18 @@ public class LoginActionsService { public boolean check(String code) { if (!checkSsl()) { event.error(Errors.SSL_REQUIRED); - response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED); return false; } if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); - response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED); return false; } clientCode = ClientSessionCode.parse(code, session, realm); if (clientCode == null) { event.error(Errors.INVALID_CODE); - response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNKNOWN_CODE); return false; } return true; @@ -224,7 +217,7 @@ public class LoginActionsService { clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); } - LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) + LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo, headers) .setClientSessionCode(clientSessionCode.getCode()); return forms.createLogin(); @@ -242,7 +235,7 @@ public class LoginActionsService { event.event(EventType.REGISTER); if (!realm.isRegistrationAllowed()) { event.error(Errors.REGISTRATION_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Registration not allowed"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REGISTRATION_NOT_ALLOWED); } Checks checks = new Checks(); @@ -256,7 +249,7 @@ public class LoginActionsService { authManager.expireIdentityCookie(realm, uriInfo, clientConnection); - return Flows.forms(session, realm, clientSession.getClient(), uriInfo) + return Flows.forms(session, realm, clientSession.getClient(), uriInfo, headers) .setClientSessionCode(clientSessionCode.getCode()) .createRegistration(); } @@ -276,17 +269,17 @@ public class LoginActionsService { event.event(EventType.LOGIN); if (!checkSsl()) { event.error(Errors.SSL_REQUIRED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED); } if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED); } ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm); if (clientCode == null) { event.error(Errors.INVALID_CODE); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNKNOWN_CODE); } ClientSessionModel clientSession = clientCode.getClientSession(); @@ -295,7 +288,7 @@ public class LoginActionsService { if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE) || clientSession.getUserSession() != null) { clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE); event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE); - return Flows.forms(this.session, realm, clientSession.getClient(), uriInfo).setError(Messages.EXPIRED_CODE) + return Flows.forms(this.session, realm, clientSession.getClient(), uriInfo, headers).setError(Messages.EXPIRED_CODE) .setClientSessionCode(clientCode.getCode()) .createLogin(); } @@ -319,17 +312,18 @@ public class LoginActionsService { ClientModel client = clientSession.getClient(); if (client == null) { event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNKNOWN_LOGIN_REQUESTER); } if (!client.isEnabled()) { event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.LOGIN_REQUESTER_NOT_ENABLED); } if (formData.containsKey("cancel")) { event.error(Errors.REJECTED_BY_USER); LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); protocol.setRealm(realm) + .setHttpHeaders(headers) .setUriInfo(uriInfo); return protocol.cancelLogin(clientSession); } @@ -356,14 +350,14 @@ public class LoginActionsService { return authManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); case ACCOUNT_TEMPORARILY_DISABLED: event.error(Errors.USER_TEMPORARILY_DISABLED); - return Flows.forms(this.session, realm, client, uriInfo) + return Flows.forms(this.session, realm, client, uriInfo, headers) .setError(Messages.ACCOUNT_TEMPORARILY_DISABLED) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) .createLogin(); case ACCOUNT_DISABLED: event.error(Errors.USER_DISABLED); - return Flows.forms(this.session, realm, client, uriInfo) + return Flows.forms(this.session, realm, client, uriInfo, headers) .setError(Messages.ACCOUNT_DISABLED) .setClientSessionCode(clientCode.getCode()) .setFormData(formData).createLogin(); @@ -373,19 +367,19 @@ public class LoginActionsService { String passwordToken = new JWSBuilder().jsonContent(new PasswordToken(realm.getName(), user.getId())).rsa256(realm.getPrivateKey()); formData.add(CredentialRepresentation.PASSWORD_TOKEN, passwordToken); - return Flows.forms(this.session, realm, client, uriInfo) + return Flows.forms(this.session, realm, client, uriInfo, headers) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) .createLoginTotp(); case INVALID_USER: event.error(Errors.USER_NOT_FOUND); - return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.INVALID_USER) + return Flows.forms(this.session, realm, client, uriInfo, headers).setError(Messages.INVALID_USER) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) .createLogin(); default: event.error(Errors.INVALID_USER_CREDENTIALS); - return Flows.forms(this.session, realm, client, uriInfo).setError(Messages.INVALID_USER) + return Flows.forms(this.session, realm, client, uriInfo, headers).setError(Messages.INVALID_USER) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) .createLogin(); @@ -407,25 +401,25 @@ public class LoginActionsService { event.event(EventType.REGISTER); if (!checkSsl()) { event.error(Errors.SSL_REQUIRED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED); } if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED); } if (!realm.isRegistrationAllowed()) { event.error(Errors.REGISTRATION_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Registration not allowed"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REGISTRATION_NOT_ALLOWED); } ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm); if (clientCode == null) { event.error(Errors.INVALID_CODE); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNKNOWN_CODE); } if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE)) { event.error(Errors.INVALID_CODE); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid code, please login again through your application."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_CODE); } String username = formData.getFirst("username"); @@ -444,17 +438,17 @@ public class LoginActionsService { if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED); } ClientModel client = clientSession.getClient(); if (client == null) { event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNKNOWN_LOGIN_REQUESTER); } if (!client.isEnabled()) { event.error(Errors.CLIENT_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.LOGIN_REQUESTER_NOT_ENABLED); } @@ -464,15 +458,20 @@ public class LoginActionsService { } // Validate here, so user is not created if password doesn't validate to passwordPolicy of current realm - String error = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes); - if (error == null) { - error = Validation.validatePassword(formData, realm.getPasswordPolicy()); + String errorMessage = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes); + Object[] parameters = new Object[0]; + if (errorMessage == null) { + PasswordPolicy.Error error = Validation.validatePassword(formData, realm.getPasswordPolicy()); + if(error != null){ + errorMessage = error.getMessage(); + parameters = error.getParameters(); + } } - if (error != null) { + if (errorMessage != null) { event.error(Errors.INVALID_REGISTRATION); - return Flows.forms(session, realm, client, uriInfo) - .setError(error) + return Flows.forms(session, realm, client, uriInfo, headers) + .setError(errorMessage, parameters) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) .createRegistration(); @@ -481,7 +480,7 @@ public class LoginActionsService { // Validate that user with this username doesn't exist in realm or any federation provider if (session.users().getUserByUsername(username, realm) != null) { event.error(Errors.USERNAME_IN_USE); - return Flows.forms(session, realm, client, uriInfo) + return Flows.forms(session, realm, client, uriInfo, headers) .setError(Messages.USERNAME_EXISTS) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) @@ -491,7 +490,7 @@ public class LoginActionsService { // Validate that user with this email doesn't exist in realm or any federation provider if (session.users().getUserByEmail(email, realm) != null) { event.error(Errors.EMAIL_IN_USE); - return Flows.forms(session, realm, client, uriInfo) + return Flows.forms(session, realm, client, uriInfo, headers) .setError(Messages.EMAIL_EXISTS) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) @@ -512,9 +511,14 @@ public class LoginActionsService { boolean passwordUpdateSuccessful; String passwordUpdateError = null; + Object[] passwordUpdateErrorParameters = null; try { session.users().updateCredential(realm, user, UserCredentialModel.password(formData.getFirst("password"))); passwordUpdateSuccessful = true; + } catch (ModelException me) { + passwordUpdateSuccessful = false; + passwordUpdateError = me.getMessage(); + passwordUpdateErrorParameters = me.getParameters(); } catch (Exception ape) { passwordUpdateSuccessful = false; passwordUpdateError = ape.getMessage(); @@ -523,8 +527,8 @@ public class LoginActionsService { // User already registered, but force him to update password if (!passwordUpdateSuccessful) { user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - return Flows.forms(session, realm, client, uriInfo) - .setError(passwordUpdateError) + return Flows.forms(session, realm, client, uriInfo, headers) + .setError(passwordUpdateError, passwordUpdateErrorParameters) .setClientSessionCode(clientCode.getCode()) .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); } @@ -552,7 +556,7 @@ public class LoginActionsService { if (!checkSsl()) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED); } String code = formData.getFirst("code"); @@ -560,7 +564,7 @@ public class LoginActionsService { ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); if (accessCode == null || !accessCode.isValid(ClientSessionModel.Action.OAUTH_GRANT)) { event.error(Errors.INVALID_CODE); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid access code."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_ACCESS_CODE); } ClientSessionModel clientSession = accessCode.getClientSession(); event.detail(Details.CODE_ID, clientSession.getId()); @@ -582,14 +586,15 @@ public class LoginActionsService { } if (!AuthenticationManager.isSessionValid(realm, userSession)) { - AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection); + AuthenticationManager.logout(session, realm, userSession, uriInfo, clientConnection, headers); event.error(Errors.INVALID_CODE); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Session not active"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE); } event.session(userSession); LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); protocol.setRealm(realm) + .setHttpHeaders(headers) .setUriInfo(uriInfo); if (formData.containsKey("cancel")) { event.error(Errors.REJECTED_BY_USER); @@ -623,7 +628,7 @@ public class LoginActionsService { String error = Validation.validateUpdateProfileForm(formData); if (error != null) { - return Flows.forms(session, realm, null, uriInfo).setUser(user).setError(error) + return Flows.forms(session, realm, null, uriInfo, headers).setUser(user).setError(error) .setClientSessionCode(accessCode.getCode()) .createResponse(RequiredAction.UPDATE_PROFILE); } @@ -641,7 +646,7 @@ public class LoginActionsService { // check for duplicated email if (userByEmail != null && !userByEmail.getId().equals(user.getId())) { - return Flows.forms(session, realm, null, uriInfo).setUser(user).setError(Messages.EMAIL_EXISTS) + return Flows.forms(session, realm, null, uriInfo, headers).setUser(user).setError(Messages.EMAIL_EXISTS) .setClientSessionCode(accessCode.getCode()) .createResponse(RequiredAction.UPDATE_PROFILE); } @@ -680,7 +685,7 @@ public class LoginActionsService { String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); - LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user); + LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo, headers).setUser(user); if (Validation.isEmpty(totp)) { return loginForms.setError(Messages.MISSING_TOTP) .setClientSessionCode(accessCode.getCode()) @@ -725,7 +730,7 @@ public class LoginActionsService { String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); - LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo).setUser(user); + LoginFormsProvider loginForms = Flows.forms(session, realm, null, uriInfo, headers).setUser(user); if (Validation.isEmpty(passwordNew)) { return loginForms.setError(Messages.MISSING_PASSWORD) .setClientSessionCode(accessCode.getCode()) @@ -738,6 +743,10 @@ public class LoginActionsService { try { session.users().updateCredential(realm, user, UserCredentialModel.password(passwordNew)); + } catch (ModelException me) { + return loginForms.setError(me.getMessage(), me.getParameters()) + .setClientSessionCode(accessCode.getCode()) + .createResponse(RequiredAction.UPDATE_PASSWORD); } catch (Exception ape) { return loginForms.setError(ape.getMessage()) .setClientSessionCode(accessCode.getCode()) @@ -751,7 +760,7 @@ public class LoginActionsService { if (clientSession.getAction().equals(ClientSessionModel.Action.RECOVER_PASSWORD)) { String actionCookieValue = getActionCookie(); if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) { - return Flows.forms(session, realm, clientSession.getClient(), uriInfo).setSuccess("passwordUpdated").createInfoPage(); + return Flows.forms(session, realm, clientSession.getClient(), uriInfo, headers).setSuccess(Messages.ACCOUNT_PASSWORD_UPDATED).createInfoPage(); } } @@ -783,7 +792,7 @@ public class LoginActionsService { String actionCookieValue = getActionCookie(); if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) { - return Flows.forms(session, realm, clientSession.getClient(), uriInfo).setSuccess("emailVerified").createInfoPage(); + return Flows.forms(session, realm, clientSession.getClient(), uriInfo, headers).setSuccess(Messages.EMAIL_VERIFIED).createInfoPage(); } event = event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN); @@ -801,7 +810,7 @@ public class LoginActionsService { createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); - return Flows.forms(session, realm, null, uriInfo) + return Flows.forms(session, realm, null, uriInfo, headers) .setClientSessionCode(accessCode.getCode()) .setUser(userSession.getUser()) .createResponse(RequiredAction.VERIFY_EMAIL); @@ -818,11 +827,11 @@ public class LoginActionsService { return checks.response; } ClientSessionCode accessCode = checks.clientCode; - return Flows.forms(session, realm, null, uriInfo) + return Flows.forms(session, realm, null, uriInfo, headers) .setClientSessionCode(accessCode.getCode()) .createResponse(RequiredAction.UPDATE_PASSWORD); } else { - return Flows.forms(session, realm, null, uriInfo) + return Flows.forms(session, realm, null, uriInfo, headers) .setClientSessionCode(code) .createPasswordReset(); } @@ -835,16 +844,16 @@ public class LoginActionsService { final MultivaluedMap formData) { event.event(EventType.SEND_RESET_PASSWORD); if (!checkSsl()) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED); } if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED); } ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); if (accessCode == null) { event.error(Errors.INVALID_CODE); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNKNOWN_CODE); } ClientSessionModel clientSession = accessCode.getClientSession(); @@ -852,12 +861,10 @@ public class LoginActionsService { ClientModel client = clientSession.getClient(); if (client == null) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, - "Unknown login requester."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.UNKNOWN_LOGIN_REQUESTER); } if (!client.isEnabled()) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, - "Login requester not enabled."); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.LOGIN_REQUESTER_NOT_ENABLED); } event.client(client.getClientId()) @@ -900,7 +907,7 @@ public class LoginActionsService { } catch (EmailException e) { event.error(Errors.EMAIL_SEND_FAILED); logger.error("Failed to send password reset email", e); - return Flows.forms(this.session, realm, client, uriInfo).setError("emailSendError") + return Flows.forms(this.session, realm, client, uriInfo, headers).setError(Messages.EMAIL_SENT_ERROR) .setClientSessionCode(accessCode.getCode()) .createErrorPage(); } @@ -908,7 +915,7 @@ public class LoginActionsService { createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); } - return Flows.forms(session, realm, client, uriInfo).setSuccess("emailSent").setClientSessionCode(accessCode.getCode()).createPasswordReset(); + return Flows.forms(session, realm, client, uriInfo, headers).setSuccess(Messages.EMAIL_SENT).setClientSessionCode(accessCode.getCode()).createPasswordReset(); } private String getActionCookie() { diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java index 8c9e4d4b65..3d0697db8a 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Flows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Flows.java @@ -26,6 +26,7 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -37,16 +38,16 @@ public class Flows { private Flows() { } - public static LoginFormsProvider forms(KeycloakSession session, RealmModel realm, ClientModel client, UriInfo uriInfo) { - return session.getProvider(LoginFormsProvider.class).setRealm(realm).setUriInfo(uriInfo).setClient(client); + public static LoginFormsProvider forms(KeycloakSession session, RealmModel realm, ClientModel client, UriInfo uriInfo, HttpHeaders headers) { + return session.getProvider(LoginFormsProvider.class).setRealm(realm).setUriInfo(uriInfo).setClient(client).setHttpHeaders(headers); } public static ErrorFlows errors() { return new ErrorFlows(); } - public static Response forwardToSecurityFailurePage(KeycloakSession session, RealmModel realm, UriInfo uriInfo, String message) { - return Flows.forms(session, realm, null, uriInfo).setError(message).createErrorPage(); + public static Response forwardToSecurityFailurePage(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String message, Object ... parameters) { + return Flows.forms(session, realm, null, uriInfo, headers).setError(message,parameters).createErrorPage(); } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java index 7889388fc9..ab56f4d7db 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java @@ -190,6 +190,10 @@ public class Urls { return requiredActionsBase(baseUri).path(LoginActionsService.class, "processConsent").build(realmId); } + public static String localeCookiePath(URI baseUri, String realmName){ + return realmBase(baseUri).path(realmName).build().getRawPath(); + } + public static URI themeRoot(URI baseUri) { return themeBase(baseUri).path(Version.RESOURCES_VERSION).build(); } diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java index d306dc050d..f72a63023a 100755 --- a/services/src/main/java/org/keycloak/services/validation/Validation.java +++ b/services/src/main/java/org/keycloak/services/validation/Validation.java @@ -48,7 +48,7 @@ public class Validation { return null; } - public static String validatePassword(MultivaluedMap formData, PasswordPolicy policy) { + public static PasswordPolicy.Error validatePassword(MultivaluedMap formData, PasswordPolicy policy) { return policy.validate(formData.getFirst("username"), formData.getFirst("password")); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 6497cb50f1..4743826502 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -283,7 +283,7 @@ public class AccountTest { // All fields are required, so there should be an error when something is missing. profilePage.updateProfile("", "New last", "new@email.com"); - Assert.assertEquals("Please specify first name", profilePage.getError()); + Assert.assertEquals("Please specify first name.", profilePage.getError()); Assert.assertEquals("", profilePage.getFirstName()); Assert.assertEquals("New last", profilePage.getLastName()); Assert.assertEquals("new@email.com", profilePage.getEmail()); @@ -292,7 +292,7 @@ public class AccountTest { profilePage.updateProfile("New first", "", "new@email.com"); - Assert.assertEquals("Please specify last name", profilePage.getError()); + Assert.assertEquals("Please specify last name.", profilePage.getError()); Assert.assertEquals("New first", profilePage.getFirstName()); Assert.assertEquals("", profilePage.getLastName()); Assert.assertEquals("new@email.com", profilePage.getEmail()); @@ -301,7 +301,7 @@ public class AccountTest { profilePage.updateProfile("New first", "New last", ""); - Assert.assertEquals("Please specify email", profilePage.getError()); + Assert.assertEquals("Please specify email.", profilePage.getError()); Assert.assertEquals("New first", profilePage.getFirstName()); Assert.assertEquals("New last", profilePage.getLastName()); Assert.assertEquals("", profilePage.getEmail()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java index 1d6665171b..0d6b4cfefc 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java @@ -236,7 +236,7 @@ public class RequiredActionEmailVerificationTest { events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent(); assertTrue(infoPage.isCurrent()); - assertEquals("Email verified", infoPage.getInfo()); + assertEquals("Your email address has been verified.", infoPage.getInfo()); loginPage.open(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index d832f967ee..e5e4c3c3f3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -108,7 +108,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - Assert.assertEquals("Please specify first name", updateProfilePage.getError()); + Assert.assertEquals("Please specify first name.", updateProfilePage.getError()); events.assertEmpty(); } @@ -125,7 +125,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - Assert.assertEquals("Please specify last name", updateProfilePage.getError()); + Assert.assertEquals("Please specify last name.", updateProfilePage.getError()); events.assertEmpty(); } @@ -142,7 +142,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - Assert.assertEquals("Please specify email", updateProfilePage.getError()); + Assert.assertEquals("Please specify email.", updateProfilePage.getError()); events.assertEmpty(); } @@ -159,7 +159,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - Assert.assertEquals("Email already exists", updateProfilePage.getError()); + Assert.assertEquals("Email already exists.", updateProfilePage.getError()); events.assertEmpty(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index b1ae4cbe70..6579d4e2a5 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -286,7 +286,7 @@ public abstract class AbstractIdentityProviderTest { assertNotNull(element); - assertEquals("Email already exists", element.getText()); + assertEquals("Email already exists.", element.getText()); this.updateProfilePage.assertCurrent(); this.updateProfilePage.update("Test", "User", "test-user@redhat.com"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java index 611c6bd10e..5f484dc8b2 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java @@ -236,12 +236,12 @@ public class FederationProvidersIntegrationTest { // check existing username registerPage.register("firstName", "lastName", "email@mail.cz", "existing", "Password1", "Password1"); registerPage.assertCurrent(); - Assert.assertEquals("Username already exists", registerPage.getError()); + Assert.assertEquals("Username already exists.", registerPage.getError()); // Check existing email registerPage.register("firstName", "lastName", "existing@email.org", "nonExisting", "Password1", "Password1"); registerPage.assertCurrent(); - Assert.assertEquals("Email already exists", registerPage.getError()); + Assert.assertEquals("Email already exists.", registerPage.getError()); } @Test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 75a83f4f8a..8c5a2f8b22 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -162,7 +162,7 @@ public class LoginTest { loginPage.assertCurrent(); - Assert.assertEquals("Account is disabled, contact admin", loginPage.getError()); + Assert.assertEquals("Account is disabled, contact admin.", loginPage.getError()); events.expectLogin().user(userId).session((String) null).error("user_disabled").detail(Details.USERNAME, "login-test").assertEvent(); } finally { @@ -316,7 +316,7 @@ public class LoginTest { loginPage.login("login@test.com", "password"); loginPage.assertCurrent(); - Assert.assertEquals("Login timeout. Please login again", loginPage.getError()); + Assert.assertEquals("Login timeout. Please login again.", loginPage.getError()); events.expectLogin().user((String) null).session((String) null).error("expired_code").clearDetails().detail(Details.CODE_ID, AssertEvents.isCodeId()).assertEvent(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index 2d0657a438..b2ac90e04e 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -78,7 +78,7 @@ public class RegisterTest { registerPage.register("firstName", "lastName", "registerExistingUser@email", "test-user@localhost", "password", "password"); registerPage.assertCurrent(); - Assert.assertEquals("Username already exists", registerPage.getError()); + Assert.assertEquals("Username already exists.", registerPage.getError()); events.expectRegister("test-user@localhost", "registerExistingUser@email").user((String) null).error("username_in_use").assertEvent(); } @@ -92,7 +92,7 @@ public class RegisterTest { registerPage.register("firstName", "lastName", "registerUserInvalidPasswordConfirm@email", "registerUserInvalidPasswordConfirm", "password", "invalid"); registerPage.assertCurrent(); - Assert.assertEquals("Password confirmation doesn't match", registerPage.getError()); + Assert.assertEquals("Password confirmation doesn't match.", registerPage.getError()); events.expectRegister("registerUserInvalidPasswordConfirm", "registerUserInvalidPasswordConfirm@email").user((String) null).error("invalid_registration").assertEvent(); } @@ -157,7 +157,7 @@ public class RegisterTest { registerPage.register("firstName", "lastName", "registerUserMissingUsername@email", null, "password", "password"); registerPage.assertCurrent(); - Assert.assertEquals("Please specify username", registerPage.getError()); + Assert.assertEquals("Please specify username.", registerPage.getError()); events.expectRegister(null, "registerUserMissingUsername@email").removeDetail("username").error("invalid_registration").assertEvent(); } @@ -170,12 +170,12 @@ public class RegisterTest { registerPage.register("firstName", "lastName", null, "registerUserMissingEmail", "password", "password"); registerPage.assertCurrent(); - Assert.assertEquals("Please specify email", registerPage.getError()); + Assert.assertEquals("Please specify email.", registerPage.getError()); events.expectRegister("registerUserMissingEmail", null).removeDetail("email").error("invalid_registration").assertEvent(); registerPage.register("firstName", "lastName", "registerUserInvalidEmailemail", "registerUserInvalidEmail", "password", "password"); registerPage.assertCurrent(); - Assert.assertEquals("Invalid email address", registerPage.getError()); + Assert.assertEquals("Invalid email address.", registerPage.getError()); events.expectRegister("registerUserInvalidEmail", "registerUserInvalidEmailemail").error("invalid_registration").assertEvent(); } @@ -205,7 +205,7 @@ public class RegisterTest { registerPage.registerWithEmailAsUsername("firstName", "lastName", "test-user@localhost", "password", "password"); registerPage.assertCurrent(); - Assert.assertEquals("Username already exists", registerPage.getError()); + Assert.assertEquals("Username already exists.", registerPage.getError()); events.expectRegister("test-user@localhost", "test-user@localhost").user((String) null).error("username_in_use").assertEvent(); } finally { @@ -224,12 +224,12 @@ public class RegisterTest { registerPage.registerWithEmailAsUsername("firstName", "lastName", null, "password", "password"); registerPage.assertCurrent(); - Assert.assertEquals("Please specify email", registerPage.getError()); + Assert.assertEquals("Please specify email.", registerPage.getError()); events.expectRegister(null, null).removeDetail("username").removeDetail("email").error("invalid_registration").assertEvent(); registerPage.registerWithEmailAsUsername("firstName", "lastName", "registerUserInvalidEmailemail", "password", "password"); registerPage.assertCurrent(); - Assert.assertEquals("Invalid email address", registerPage.getError()); + Assert.assertEquals("Invalid email address.", registerPage.getError()); events.expectRegister("registerUserInvalidEmailemail", "registerUserInvalidEmailemail").error("invalid_registration").assertEvent(); } finally { configureRelamRegistrationEmailAsUsername(false); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 7582e2094f..a96b660a54 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -504,7 +504,7 @@ public class ResetPasswordTest { events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).session(sessionId).detail(Details.USERNAME, username).assertEvent(); assertTrue(infoPage.isCurrent()); - assertEquals("Password updated", infoPage.getInfo()); + assertEquals("Your password has been updated", infoPage.getInfo()); loginPage.open(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java index c75a030821..9aa884ffe8 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java @@ -108,7 +108,7 @@ public class OAuthRedirectUriTest { oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); + Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); } finally { keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -133,7 +133,7 @@ public class OAuthRedirectUriTest { oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); + Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); } finally { keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -158,7 +158,7 @@ public class OAuthRedirectUriTest { oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); + Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); } finally { keycloakRule.update(new KeycloakRule.KeycloakSetup() { @Override @@ -184,7 +184,7 @@ public class OAuthRedirectUriTest { oauth.openLoginForm(); Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); + Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); } @Test @@ -244,7 +244,7 @@ public class OAuthRedirectUriTest { Assert.assertTrue(loginPage.isCurrent()); } else { Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("Invalid redirect_uri", errorPage.getError()); + Assert.assertEquals("Invalid parameter: redirect_uri", errorPage.getError()); } if (expectValid) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java index 1766a8766c..fc83347637 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java @@ -56,7 +56,7 @@ public class LoginPage extends AbstractPage { @FindBy(linkText = "Register") private WebElement registerLink; - @FindBy(linkText = "Password") + @FindBy(linkText = "Forgot Password?") private WebElement resetPasswordLink; @FindBy(linkText = "Username")