From a3d08e7191ae1c229217a4da49506bf3c4ab54c5 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 19 May 2014 12:58:45 +0100 Subject: [PATCH] Added theme support to emails --- docbook/reference/en/en-US/modules/themes.xml | 12 +- .../java/org/keycloak/account/Account.java | 38 -- .../org/keycloak/account/AccountLoader.java | 17 - .../org/keycloak/account/AccountProvider.java | 33 +- .../account/AccountProviderFactory.java | 10 + .../java/org/keycloak/account/AccountSpi.java | 27 ++ .../services/org.keycloak.provider.Spi | 1 + .../account/freemarker/FreeMarkerAccount.java | 201 ----------- .../freemarker/FreeMarkerAccountProvider.java | 202 ++++++++++- .../FreeMarkerAccountProviderFactory.java | 33 ++ .../account/freemarker/model/MessageBean.java | 12 +- .../org.keycloak.account.AccountProvider | 1 - ...rg.keycloak.account.AccountProviderFactory | 1 + ...Loader.java => ExtendingThemeManager.java} | 70 +++- .../java/org/keycloak/freemarker/Theme.java | 2 +- .../keycloak/freemarker/ThemeProvider.java | 4 +- .../freemarker/ThemeProviderFactory.java | 9 + .../org/keycloak/freemarker/ThemeSpi.java | 25 ++ .../services/org.keycloak.provider.Spi | 1 + .../theme/DefaultKeycloakThemeProvider.java | 8 + .../DefaultKeycloakThemeProviderFactory.java | 35 ++ .../keycloak/theme/FolderThemeProvider.java | 11 +- .../theme/FolderThemeProviderFactory.java | 41 +++ .../org.keycloak.freemarker.ThemeProvider | 2 - ...g.keycloak.freemarker.ThemeProviderFactory | 2 + .../email/keycloak/email-verification.ftl | 5 + .../keycloak/messages/messages.properties | 2 + .../theme/email/keycloak/password-reset.ftl | 5 + forms/email-api/pom.xml | 44 +++ .../org/keycloak}/email/EmailException.java | 5 +- .../org/keycloak/email/EmailProvider.java | 20 ++ .../keycloak/email/EmailProviderFactory.java | 9 + .../java/org/keycloak/email/EmailSpi.java | 25 ++ .../services/org.keycloak.provider.Spi | 1 + forms/email-freemarker/pom.xml | 71 ++++ .../freemarker/FreeMarkerEmailProvider.java | 140 ++++++++ .../FreeMarkerEmailProviderFactory.java | 31 ++ .../org.keycloak.email.EmailProviderFactory | 1 + .../java/org/keycloak/login/LoginForms.java | 52 --- .../org/keycloak/login/LoginFormsLoader.java | 17 - .../keycloak/login/LoginFormsProvider.java | 73 +++- .../login/LoginFormsProviderFactory.java | 11 + .../org/keycloak/login/LoginFormsSpi.java | 25 ++ .../services/org.keycloak.provider.Spi | 1 + forms/login-freemarker/pom.xml | 6 + .../freemarker/FreeMarkerLoginForms.java | 287 --------------- .../FreeMarkerLoginFormsProvider.java | 333 +++++++++++++++++- .../FreeMarkerLoginFormsProviderFactory.java | 31 ++ .../login/freemarker/model/MessageBean.java | 12 +- .../org.keycloak.login.LoginFormsProvider | 1 - ...g.keycloak.login.LoginFormsProviderFactory | 1 + forms/pom.xml | 2 + pom.xml | 5 + .../ups/security/AerogearThemeProvider.java | 4 + server/pom.xml | 10 + .../resources/META-INF/keycloak-server.json | 12 + services/pom.xml | 6 + .../keycloak/services/email/EmailSender.java | 162 --------- .../managers/AuthenticationManager.java | 2 +- .../services/resources/AccountService.java | 20 +- .../resources/RequiredActionsService.java | 48 +-- .../services/resources/SocialResource.java | 17 +- .../services/resources/ThemeResource.java | 12 +- .../services/resources/TokenService.java | 34 +- .../resources/admin/AdminConsole.java | 6 +- .../resources/admin/RealmAdminResource.java | 2 +- .../resources/admin/UsersResource.java | 22 +- .../services/resources/flows/Flows.java | 12 +- .../services/resources/flows/OAuthFlows.java | 16 +- .../services/email/EmailSenderTest.java | 87 ----- testsuite/integration/pom.xml | 10 + .../resources/META-INF/keycloak-server.json | 12 + .../RequiredActionEmailVerificationTest.java | 20 +- .../testsuite/forms/ResetPasswordTest.java | 5 +- .../org/keycloak/testsuite/util/MailUtil.java | 21 ++ 75 files changed, 1513 insertions(+), 1041 deletions(-) delete mode 100644 forms/account-api/src/main/java/org/keycloak/account/Account.java delete mode 100644 forms/account-api/src/main/java/org/keycloak/account/AccountLoader.java create mode 100644 forms/account-api/src/main/java/org/keycloak/account/AccountProviderFactory.java create mode 100644 forms/account-api/src/main/java/org/keycloak/account/AccountSpi.java create mode 100644 forms/account-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi delete mode 100755 forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java mode change 100644 => 100755 forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java create mode 100644 forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProviderFactory.java delete mode 100644 forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider create mode 100644 forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory rename forms/common-freemarker/src/main/java/org/keycloak/freemarker/{ThemeLoader.java => ExtendingThemeManager.java} (73%) mode change 100755 => 100644 create mode 100644 forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProviderFactory.java create mode 100644 forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeSpi.java create mode 100644 forms/common-freemarker/src/main/resources/META-INF/services/org.keycloak.provider.Spi create mode 100644 forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProviderFactory.java create mode 100644 forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProviderFactory.java delete mode 100644 forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider create mode 100644 forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory create mode 100644 forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl create mode 100755 forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages.properties create mode 100644 forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl create mode 100755 forms/email-api/pom.xml rename {services/src/main/java/org/keycloak/services => forms/email-api/src/main/java/org/keycloak}/email/EmailException.java (61%) create mode 100644 forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java create mode 100644 forms/email-api/src/main/java/org/keycloak/email/EmailProviderFactory.java create mode 100644 forms/email-api/src/main/java/org/keycloak/email/EmailSpi.java create mode 100644 forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi create mode 100755 forms/email-freemarker/pom.xml create mode 100644 forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java create mode 100644 forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProviderFactory.java create mode 100644 forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory delete mode 100755 forms/login-api/src/main/java/org/keycloak/login/LoginForms.java delete mode 100644 forms/login-api/src/main/java/org/keycloak/login/LoginFormsLoader.java create mode 100755 forms/login-api/src/main/java/org/keycloak/login/LoginFormsProviderFactory.java create mode 100644 forms/login-api/src/main/java/org/keycloak/login/LoginFormsSpi.java create mode 100644 forms/login-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi delete mode 100755 forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java create mode 100755 forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProviderFactory.java delete mode 100644 forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider create mode 100644 forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory delete mode 100755 services/src/main/java/org/keycloak/services/email/EmailSender.java delete mode 100755 services/src/test/java/org/keycloak/services/email/EmailSenderTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java diff --git a/docbook/reference/en/en-US/modules/themes.xml b/docbook/reference/en/en-US/modules/themes.xml index 5bb39cb4a9..5e4ff8a0ed 100755 --- a/docbook/reference/en/en-US/modules/themes.xml +++ b/docbook/reference/en/en-US/modules/themes.xml @@ -148,26 +148,26 @@ Account SPI The Account SPI allows implementing the account management pages using whatever web framework or templating - engine you want. To create an Account provider implement org.keycloak.account.AccountProvider - and org.keycloak.account.Account in forms/account-api. + engine you want. To create an Account provider implement org.keycloak.account.AccountProviderFactory + and org.keycloak.account.AccountProvider in forms/account-api. Keycloaks default account management provider is built on the FreeMarker template engine (forms/account-freemarker). To make sure your provider is loaded you will either need to delete standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-account-freemarker-1.0-beta-1-SNAPSHOT.jar - or disable it with the system property org.keycloak.account.freemarker.FreeMarkerAccountProvider. + or disable it with the system property org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory.
Login SPI The Login SPI allows implementing the login forms using whatever web framework or templating - engine you want. To create a Login forms provider implement org.keycloak.login.LoginFormsProvider - and org.keycloak.login.LoginForms in forms/login-api. + engine you want. To create a Login forms provider implement org.keycloak.login.LoginFormsProviderFactory + and org.keycloak.login.LoginFormsProvider in forms/login-api. Keycloaks default login forms provider is built on the FreeMarker template engine (forms/login-freemarker). To make sure your provider is loaded you will either need to delete standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-login-freemarker-1.0-beta-1-SNAPSHOT.jar - or disable it with the system property org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider. + or disable it with the system property org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory.
diff --git a/forms/account-api/src/main/java/org/keycloak/account/Account.java b/forms/account-api/src/main/java/org/keycloak/account/Account.java deleted file mode 100644 index fde532463d..0000000000 --- a/forms/account-api/src/main/java/org/keycloak/account/Account.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.keycloak.account; - -import org.keycloak.audit.Event; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; - -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import java.util.List; - -/** - * @author Stian Thorgersen - */ -public interface Account { - - Response createResponse(AccountPages page); - - Account setError(String message); - - Account setSuccess(String message); - - Account setWarning(String message); - - Account setUser(UserModel user); - - Account setStatus(Response.Status status); - - Account setRealm(RealmModel realm); - - Account setReferrer(String[] referrer); - - Account setEvents(List events); - - Account setSessions(List sessions); - - Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported); -} diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountLoader.java b/forms/account-api/src/main/java/org/keycloak/account/AccountLoader.java deleted file mode 100644 index 8f9fbec94b..0000000000 --- a/forms/account-api/src/main/java/org/keycloak/account/AccountLoader.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.keycloak.account; - -import java.util.ServiceLoader; - -/** - * @author Stian Thorgersen - */ -public class AccountLoader { - - private AccountLoader() { - } - - public static AccountProvider load() { - return ServiceLoader.load(AccountProvider.class).iterator().next(); - } - -} 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 37ffe89b21..f021e84777 100644 --- 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,12 +1,41 @@ package org.keycloak.account; +import org.keycloak.audit.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.Response; import javax.ws.rs.core.UriInfo; +import java.util.List; /** * @author Stian Thorgersen */ -public interface AccountProvider { +public interface AccountProvider extends Provider { - public Account createAccount(UriInfo uriInfo); + AccountProvider setUriInfo(UriInfo uriInfo); + Response createResponse(AccountPages page); + + AccountProvider setError(String message); + + AccountProvider setSuccess(String message); + + AccountProvider setWarning(String message); + + AccountProvider setUser(UserModel user); + + AccountProvider setStatus(Response.Status status); + + AccountProvider setRealm(RealmModel realm); + + AccountProvider setReferrer(String[] referrer); + + AccountProvider setEvents(List events); + + AccountProvider setSessions(List sessions); + + AccountProvider setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported); } diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountProviderFactory.java b/forms/account-api/src/main/java/org/keycloak/account/AccountProviderFactory.java new file mode 100644 index 0000000000..a1ef25756a --- /dev/null +++ b/forms/account-api/src/main/java/org/keycloak/account/AccountProviderFactory.java @@ -0,0 +1,10 @@ +package org.keycloak.account; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stian Thorgersen + */ +public interface AccountProviderFactory extends ProviderFactory { + +} diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountSpi.java b/forms/account-api/src/main/java/org/keycloak/account/AccountSpi.java new file mode 100644 index 0000000000..e956b14e61 --- /dev/null +++ b/forms/account-api/src/main/java/org/keycloak/account/AccountSpi.java @@ -0,0 +1,27 @@ +package org.keycloak.account; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class AccountSpi implements Spi { + + @Override + public String getName() { + return "account"; + } + + @Override + public Class getProviderClass() { + return AccountProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return AccountProviderFactory.class; + } + +} diff --git a/forms/account-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/account-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..8ab9e170c0 --- /dev/null +++ b/forms/account-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.account.AccountSpi \ No newline at end of file diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java deleted file mode 100755 index 7313b5c930..0000000000 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java +++ /dev/null @@ -1,201 +0,0 @@ -package org.keycloak.account.freemarker; - -import org.jboss.logging.Logger; -import org.keycloak.account.Account; -import org.keycloak.account.AccountPages; -import org.keycloak.account.freemarker.model.AccountBean; -import org.keycloak.account.freemarker.model.AccountSocialBean; -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.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.audit.Event; -import org.keycloak.freemarker.FreeMarkerException; -import org.keycloak.freemarker.FreeMarkerUtil; -import org.keycloak.freemarker.Theme; -import org.keycloak.freemarker.ThemeLoader; -import org.keycloak.models.ApplicationModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; - -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; -import java.io.IOException; -import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -/** - * @author Stian Thorgersen - */ -public class FreeMarkerAccount implements Account { - - private static final Logger logger = Logger.getLogger(FreeMarkerAccount.class); - - private UserModel user; - private Response.Status status = Response.Status.OK; - private RealmModel realm; - private String[] referrer; - private List events; - private List sessions; - private boolean social; - private boolean audit; - private boolean passwordUpdateSupported; - - public static enum MessageType {SUCCESS, WARNING, ERROR} - - private UriInfo uriInfo; - - private String message; - private MessageType messageType; - - public FreeMarkerAccount(UriInfo uriInfo) { - this.uriInfo = uriInfo; - } - - @Override - public Response createResponse(AccountPages page) { - Map attributes = new HashMap(); - - Theme theme; - try { - theme = ThemeLoader.createTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT); - } catch (FreeMarkerException e) { - logger.error("Failed to create theme", e); - return Response.serverError().build(); - } - - try { - attributes.put("properties", theme.getProperties()); - } catch (IOException e) { - logger.warn("Failed to load properties", e); - } - - Properties messages; - try { - messages = theme.getMessages(); - attributes.put("rb", messages); - } catch (IOException e) { - logger.warn("Failed to load messages", e); - messages = new Properties(); - } - - URI baseUri = uriInfo.getBaseUri(); - UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); - for (Map.Entry> e : uriInfo.getQueryParameters().entrySet()) { - baseUriBuilder.queryParam(e.getKey(), e.getValue().toArray()); - } - URI baseQueryUri = baseUriBuilder.build(); - - if (message != null) { - attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType)); - } - - if (referrer != null) { - attributes.put("referrer", new ReferrerBean(referrer)); - } - - attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri())); - - attributes.put("features", new FeaturesBean(social, audit, passwordUpdateSupported)); - - switch (page) { - case ACCOUNT: - attributes.put("account", new AccountBean(user)); - break; - case TOTP: - attributes.put("totp", new TotpBean(user, baseUri)); - break; - case SOCIAL: - attributes.put("social", new AccountSocialBean(realm, user, uriInfo.getBaseUri())); - break; - case LOG: - attributes.put("log", new LogBean(events)); - break; - case SESSIONS: - attributes.put("sessions", new SessionsBean(realm, sessions)); - break; - } - - try { - String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme); - return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build(); - } catch (FreeMarkerException e) { - logger.error("Failed to process template", e); - return Response.serverError().build(); - } - } - - @Override - public Account setError(String message) { - this.message = message; - this.messageType = MessageType.ERROR; - return this; - } - - @Override - public Account setSuccess(String message) { - this.message = message; - this.messageType = MessageType.SUCCESS; - return this; - } - - @Override - public Account setWarning(String message) { - this.message = message; - this.messageType = MessageType.WARNING; - return this; - } - - @Override - public Account setUser(UserModel user) { - this.user = user; - return this; - } - - @Override - public Account setRealm(RealmModel realm) { - this.realm = realm; - return this; - } - - @Override - public Account setStatus(Response.Status status) { - this.status = status; - return this; - } - - @Override - public Account setReferrer(String[] referrer) { - this.referrer = referrer; - return this; - } - - @Override - public Account setEvents(List events) { - this.events = events; - return this; - } - - @Override - public Account setSessions(List sessions) { - this.sessions = sessions; - return this; - } - - @Override - public Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) { - this.social = social; - this.audit = audit; - this.passwordUpdateSupported = passwordUpdateSupported; - return this; - } -} 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 old mode 100644 new mode 100755 index 17d8fb94ba..0400f75698 --- 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 @@ -1,18 +1,214 @@ package org.keycloak.account.freemarker; -import org.keycloak.account.Account; +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.AccountSocialBean; +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.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.audit.Event; +import org.keycloak.freemarker.ExtendingThemeManager; +import org.keycloak.freemarker.FreeMarkerException; +import org.keycloak.freemarker.FreeMarkerUtil; +import org.keycloak.freemarker.Theme; +import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.provider.ProviderSession; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; /** * @author Stian Thorgersen */ public class FreeMarkerAccountProvider implements AccountProvider { + private static final Logger logger = Logger.getLogger(FreeMarkerAccountProvider.class); + + private UserModel user; + private Response.Status status = Response.Status.OK; + private RealmModel realm; + private String[] referrer; + private List events; + private List sessions; + private boolean social; + private boolean audit; + private boolean passwordUpdateSupported; + private ProviderSession session; + + public static enum MessageType {SUCCESS, WARNING, ERROR} + + private UriInfo uriInfo; + + private String message; + private MessageType messageType; + + public FreeMarkerAccountProvider(ProviderSession session) { + this.session = session; + } + + public AccountProvider setUriInfo(UriInfo uriInfo) { + this.uriInfo = uriInfo; + return this; + } + @Override - public Account createAccount(UriInfo uriInfo) { - return new FreeMarkerAccount(uriInfo); + public Response createResponse(AccountPages page) { + Map attributes = new HashMap(); + + ExtendingThemeManager themeManager = new ExtendingThemeManager(session); + Theme theme; + try { + theme = themeManager.createTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT); + } catch (IOException e) { + logger.error("Failed to create theme", e); + return Response.serverError().build(); + } + + try { + attributes.put("properties", theme.getProperties()); + } catch (IOException e) { + logger.warn("Failed to load properties", e); + } + + Properties messages; + try { + messages = theme.getMessages(); + attributes.put("rb", messages); + } catch (IOException e) { + logger.warn("Failed to load messages", e); + messages = new Properties(); + } + + URI baseUri = uriInfo.getBaseUri(); + UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); + for (Map.Entry> e : uriInfo.getQueryParameters().entrySet()) { + baseUriBuilder.queryParam(e.getKey(), e.getValue().toArray()); + } + URI baseQueryUri = baseUriBuilder.build(); + + if (message != null) { + attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType)); + } + + if (referrer != null) { + attributes.put("referrer", new ReferrerBean(referrer)); + } + + attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri())); + + attributes.put("features", new FeaturesBean(social, audit, passwordUpdateSupported)); + + switch (page) { + case ACCOUNT: + attributes.put("account", new AccountBean(user)); + break; + case TOTP: + attributes.put("totp", new TotpBean(user, baseUri)); + break; + case SOCIAL: + attributes.put("social", new AccountSocialBean(realm, user, uriInfo.getBaseUri())); + break; + case LOG: + attributes.put("log", new LogBean(events)); + break; + case SESSIONS: + attributes.put("sessions", new SessionsBean(realm, sessions)); + break; + } + + try { + String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme); + return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build(); + } catch (FreeMarkerException e) { + logger.error("Failed to process template", e); + return Response.serverError().build(); + } + } + + @Override + public AccountProvider setError(String message) { + this.message = message; + this.messageType = MessageType.ERROR; + return this; + } + + @Override + public AccountProvider setSuccess(String message) { + this.message = message; + this.messageType = MessageType.SUCCESS; + return this; + } + + @Override + public AccountProvider setWarning(String message) { + this.message = message; + this.messageType = MessageType.WARNING; + return this; + } + + @Override + public AccountProvider setUser(UserModel user) { + this.user = user; + return this; + } + + @Override + public AccountProvider setRealm(RealmModel realm) { + this.realm = realm; + return this; + } + + @Override + public AccountProvider setStatus(Response.Status status) { + this.status = status; + return this; + } + + @Override + public AccountProvider setReferrer(String[] referrer) { + this.referrer = referrer; + return this; + } + + @Override + public AccountProvider setEvents(List events) { + this.events = events; + return this; + } + + @Override + public AccountProvider setSessions(List sessions) { + this.sessions = sessions; + return this; + } + + @Override + public AccountProvider setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) { + this.social = social; + this.audit = audit; + this.passwordUpdateSupported = passwordUpdateSupported; + return this; + } + + @Override + public void close() { } } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProviderFactory.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProviderFactory.java new file mode 100644 index 0000000000..7a3f272c7a --- /dev/null +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProviderFactory.java @@ -0,0 +1,33 @@ +package org.keycloak.account.freemarker; + +import org.keycloak.Config; +import org.keycloak.account.AccountProvider; +import org.keycloak.account.AccountProviderFactory; +import org.keycloak.provider.ProviderSession; + +import javax.ws.rs.core.UriInfo; + +/** + * @author Stian Thorgersen + */ +public class FreeMarkerAccountProviderFactory implements AccountProviderFactory { + + @Override + public AccountProvider create(ProviderSession providerSession) { + return new FreeMarkerAccountProvider(providerSession); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "freemarker"; + } + +} diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java index 08f156a4c7..6fc48be181 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java @@ -21,7 +21,7 @@ */ package org.keycloak.account.freemarker.model; -import org.keycloak.account.freemarker.FreeMarkerAccount; +import org.keycloak.account.freemarker.FreeMarkerAccountProvider; /** * @author Stian Thorgersen @@ -30,9 +30,9 @@ public class MessageBean { private String summary; - private FreeMarkerAccount.MessageType type; + private FreeMarkerAccountProvider.MessageType type; - public MessageBean(String message, FreeMarkerAccount.MessageType type) { + public MessageBean(String message, FreeMarkerAccountProvider.MessageType type) { this.summary = message; this.type = type; } @@ -46,15 +46,15 @@ public class MessageBean { } public boolean isSuccess() { - return FreeMarkerAccount.MessageType.SUCCESS.equals(this.type); + return FreeMarkerAccountProvider.MessageType.SUCCESS.equals(this.type); } public boolean isWarning() { - return FreeMarkerAccount.MessageType.WARNING.equals(this.type); + return FreeMarkerAccountProvider.MessageType.WARNING.equals(this.type); } public boolean isError() { - return FreeMarkerAccount.MessageType.ERROR.equals(this.type); + return FreeMarkerAccountProvider.MessageType.ERROR.equals(this.type); } } \ No newline at end of file diff --git a/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider b/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider deleted file mode 100644 index 2f97162ef1..0000000000 --- a/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider +++ /dev/null @@ -1 +0,0 @@ -org.keycloak.account.freemarker.FreeMarkerAccountProvider \ No newline at end of file diff --git a/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory b/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory new file mode 100644 index 0000000000..fd99df60c6 --- /dev/null +++ b/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory @@ -0,0 +1 @@ +org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory \ No newline at end of file diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeLoader.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java old mode 100755 new mode 100644 similarity index 73% rename from forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeLoader.java rename to forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java index 23f99e3567..06991e20cc --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeLoader.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java @@ -1,31 +1,35 @@ package org.keycloak.freemarker; import org.keycloak.Config; -import org.keycloak.util.ProviderLoader; +import org.keycloak.provider.ProviderSession; 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; /** * @author Stian Thorgersen */ -public class ThemeLoader { +public class ExtendingThemeManager implements ThemeProvider { - public static Theme createTheme(String name, Theme.Type type) throws FreeMarkerException { - if (name == null) { - name = Config.scope("theme").get("default"); - } + private List providers; + private String defaultTheme; - List providers = new LinkedList(); - for (ThemeProvider p : ProviderLoader.load(ThemeProvider.class)) { - providers.add(p); + public ExtendingThemeManager(ProviderSession providerSession) { + providers = new LinkedList(); + + for (ThemeProvider p : providerSession.getAllProviders(ThemeProvider.class)) { + if (!p.getClass().equals(ExtendingThemeManager.class)) { + providers.add(p); + } } Collections.sort(providers, new Comparator() { @@ -35,23 +39,37 @@ public class ThemeLoader { } }); - Theme theme = findTheme(providers, name, type); + this.defaultTheme = Config.scope("theme").get("default"); + } + + @Override + public int getProviderPriority() { + return 0; + } + + @Override + public Theme createTheme(String name, Theme.Type type) throws IOException { + if (name == null) { + name = defaultTheme; + } + + Theme theme = findTheme(name, type); if (theme.getParentName() != null) { List themes = new LinkedList(); themes.add(theme); if (theme.getImportName() != null) { String[] s = theme.getImportName().split("/"); - themes.add(findTheme(providers, s[1], Theme.Type.valueOf(s[0].toUpperCase()))); + themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase()))); } for (String parentName = theme.getParentName(); parentName != null; parentName = theme.getParentName()) { - theme = findTheme(providers, parentName, type); + theme = findTheme(parentName, type); themes.add(theme); if (theme.getImportName() != null) { String[] s = theme.getImportName().split("/"); - themes.add(findTheme(providers, s[1], Theme.Type.valueOf(s[0].toUpperCase()))); + themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase()))); } } @@ -61,7 +79,31 @@ public class ThemeLoader { } } - private static Theme findTheme(Iterable providers, String name, Theme.Type type) { + @Override + public Set nameSet(Theme.Type type) { + Set themes = new HashSet(); + for (ThemeProvider p : providers) { + themes.addAll(p.nameSet(type)); + } + return themes; + } + + @Override + public boolean hasTheme(String name, Theme.Type type) { + for (ThemeProvider p : providers) { + if (p.hasTheme(name, type)) { + return true; + } + } + return false; + } + + @Override + public void close() { + providers = null; + } + + private Theme findTheme(String name, Theme.Type type) { for (ThemeProvider p : providers) { if (p.hasTheme(name, type)) { try { 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 dd02e62ce3..14794fbcc8 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 @@ -10,7 +10,7 @@ import java.util.Properties; */ public interface Theme { - public enum Type { LOGIN, ACCOUNT, ADMIN, COMMON }; + public enum Type { LOGIN, ACCOUNT, ADMIN, EMAIL, COMMON }; public String getName(); diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java index 6ca677c741..07ee09a9d2 100644 --- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProvider.java @@ -1,12 +1,14 @@ package org.keycloak.freemarker; +import org.keycloak.provider.Provider; + import java.io.IOException; import java.util.Set; /** * @author Stian Thorgersen */ -public interface ThemeProvider { +public interface ThemeProvider extends Provider { public int getProviderPriority(); diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProviderFactory.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProviderFactory.java new file mode 100644 index 0000000000..26ce238f9e --- /dev/null +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeProviderFactory.java @@ -0,0 +1,9 @@ +package org.keycloak.freemarker; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stian Thorgersen + */ +public interface ThemeProviderFactory extends ProviderFactory { +} diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeSpi.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeSpi.java new file mode 100644 index 0000000000..c3d738bb56 --- /dev/null +++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ThemeSpi.java @@ -0,0 +1,25 @@ +package org.keycloak.freemarker; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class ThemeSpi implements Spi { + @Override + public String getName() { + return "theme"; + } + + @Override + public Class getProviderClass() { + return ThemeProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return ThemeProviderFactory.class; + } +} diff --git a/forms/common-freemarker/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/common-freemarker/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..4ba538b97d --- /dev/null +++ b/forms/common-freemarker/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.freemarker.ThemeSpi \ No newline at end of file diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProvider.java b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProvider.java index 8a8659c4d2..47f1fddb1e 100644 --- a/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProvider.java +++ b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProvider.java @@ -20,12 +20,14 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider { private static Set ACCOUNT_THEMES = new HashSet(); private static Set LOGIN_THEMES = new HashSet(); private static Set ADMIN_THEMES = new HashSet(); + private static Set EMAIL_THEMES = new HashSet(); private static Set COMMON_THEMES = new HashSet(); static { Collections.addAll(ACCOUNT_THEMES, BASE, PATTERNFLY, KEYCLOAK); Collections.addAll(LOGIN_THEMES, BASE, PATTERNFLY, KEYCLOAK); Collections.addAll(ADMIN_THEMES, BASE, PATTERNFLY, KEYCLOAK); + Collections.addAll(EMAIL_THEMES, KEYCLOAK); Collections.addAll(COMMON_THEMES, KEYCLOAK); } @@ -52,6 +54,8 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider { return ACCOUNT_THEMES; case ADMIN: return ADMIN_THEMES; + case EMAIL: + return EMAIL_THEMES; case COMMON: return COMMON_THEMES; default: @@ -64,4 +68,8 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider { return nameSet(type).contains(name); } + @Override + public void close() { + } + } diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProviderFactory.java b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProviderFactory.java new file mode 100644 index 0000000000..4c3515d963 --- /dev/null +++ b/forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProviderFactory.java @@ -0,0 +1,35 @@ +package org.keycloak.theme; + +import org.keycloak.Config; +import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.freemarker.ThemeProviderFactory; +import org.keycloak.provider.ProviderSession; + +/** + * @author Stian Thorgersen + */ +public class DefaultKeycloakThemeProviderFactory implements ThemeProviderFactory { + + private DefaultKeycloakThemeProvider themeProvider; + + @Override + public ThemeProvider create(ProviderSession providerSession) { + return themeProvider; + } + + @Override + public void init(Config.Scope config) { + themeProvider = new DefaultKeycloakThemeProvider(); + } + + @Override + public void close() { + themeProvider = null; + } + + @Override + public String getId() { + return "default"; + } + +} diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java index 101851c9b7..96cac2f631 100644 --- a/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java +++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProvider.java @@ -18,11 +18,8 @@ public class FolderThemeProvider implements ThemeProvider { private File rootDir; - public FolderThemeProvider() { - String d = Config.scope("theme").get("dir"); - if (d != null) { - rootDir = new File(d); - } + public FolderThemeProvider(File rootDir) { + this.rootDir = rootDir; } @Override @@ -75,4 +72,8 @@ public class FolderThemeProvider implements ThemeProvider { return typeDir != null && new File(typeDir, name).isDirectory(); } + @Override + public void close() { + } + } diff --git a/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProviderFactory.java b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProviderFactory.java new file mode 100644 index 0000000000..ff9dd2eac0 --- /dev/null +++ b/forms/common-themes/src/main/java/org/keycloak/theme/FolderThemeProviderFactory.java @@ -0,0 +1,41 @@ +package org.keycloak.theme; + +import org.keycloak.Config; +import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.freemarker.ThemeProviderFactory; +import org.keycloak.provider.ProviderSession; + +import java.io.File; + +/** + * @author Stian Thorgersen + */ +public class FolderThemeProviderFactory implements ThemeProviderFactory { + + private FolderThemeProvider themeProvider; + + @Override + public ThemeProvider create(ProviderSession providerSession) { + return themeProvider; + } + + @Override + public void init(Config.Scope config) { + String d = config.get("dir"); + File rootDir = null; + if (d != null) { + rootDir = new File(d); + } + themeProvider = new FolderThemeProvider(rootDir); + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "folder"; + } +} diff --git a/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider b/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider deleted file mode 100644 index f9f661b09f..0000000000 --- a/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider +++ /dev/null @@ -1,2 +0,0 @@ -org.keycloak.theme.DefaultKeycloakThemeProvider -org.keycloak.theme.FolderThemeProvider \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory b/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory new file mode 100644 index 0000000000..c1b32dce49 --- /dev/null +++ b/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory @@ -0,0 +1,2 @@ +org.keycloak.theme.DefaultKeycloakThemeProviderFactory +org.keycloak.theme.FolderThemeProviderFactory \ No newline at end of file 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 new file mode 100644 index 0000000000..38d150fd5d --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/email-verification.ftl @@ -0,0 +1,5 @@ +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 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 new file mode 100755 index 0000000000..3139aca340 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/messages/messages.properties @@ -0,0 +1,2 @@ +emailVerificationSubject=Verify email +passwordResetSubject=Reset password \ No newline at end of file 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 new file mode 100644 index 0000000000..5d277e5e2b --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/email/keycloak/password-reset.ftl @@ -0,0 +1,5 @@ +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 diff --git a/forms/email-api/pom.xml b/forms/email-api/pom.xml new file mode 100755 index 0000000000..19ef08a092 --- /dev/null +++ b/forms/email-api/pom.xml @@ -0,0 +1,44 @@ + + + + keycloak-forms + org.keycloak + 1.0-beta-1-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-email-api + Keycloak Email API + + + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-model-api + ${project.version} + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + diff --git a/services/src/main/java/org/keycloak/services/email/EmailException.java b/forms/email-api/src/main/java/org/keycloak/email/EmailException.java similarity index 61% rename from services/src/main/java/org/keycloak/services/email/EmailException.java rename to forms/email-api/src/main/java/org/keycloak/email/EmailException.java index d15c022b9b..56a0910f61 100644 --- a/services/src/main/java/org/keycloak/services/email/EmailException.java +++ b/forms/email-api/src/main/java/org/keycloak/email/EmailException.java @@ -1,4 +1,4 @@ -package org.keycloak.services.email; +package org.keycloak.email; /** * @author Stian Thorgersen @@ -9,4 +9,7 @@ public class EmailException extends Exception { super(cause); } + public EmailException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java new file mode 100644 index 0000000000..0827b9689f --- /dev/null +++ b/forms/email-api/src/main/java/org/keycloak/email/EmailProvider.java @@ -0,0 +1,20 @@ +package org.keycloak.email; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.provider.Provider; + +/** + * @author Stian Thorgersen + */ +public interface EmailProvider extends Provider { + + public EmailProvider setRealm(RealmModel realm); + + public EmailProvider setUser(UserModel user); + + public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException; + + public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException; + +} diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailProviderFactory.java b/forms/email-api/src/main/java/org/keycloak/email/EmailProviderFactory.java new file mode 100644 index 0000000000..02d7daff00 --- /dev/null +++ b/forms/email-api/src/main/java/org/keycloak/email/EmailProviderFactory.java @@ -0,0 +1,9 @@ +package org.keycloak.email; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Stian Thorgersen + */ +public interface EmailProviderFactory extends ProviderFactory { +} diff --git a/forms/email-api/src/main/java/org/keycloak/email/EmailSpi.java b/forms/email-api/src/main/java/org/keycloak/email/EmailSpi.java new file mode 100644 index 0000000000..cb2877bb6b --- /dev/null +++ b/forms/email-api/src/main/java/org/keycloak/email/EmailSpi.java @@ -0,0 +1,25 @@ +package org.keycloak.email; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class EmailSpi implements Spi { + @Override + public String getName() { + return "email"; + } + + @Override + public Class getProviderClass() { + return EmailProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return EmailProviderFactory.class; + } +} diff --git a/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..4110dbad33 --- /dev/null +++ b/forms/email-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.email.EmailSpi \ No newline at end of file diff --git a/forms/email-freemarker/pom.xml b/forms/email-freemarker/pom.xml new file mode 100755 index 0000000000..b5fb4def71 --- /dev/null +++ b/forms/email-freemarker/pom.xml @@ -0,0 +1,71 @@ + + + + keycloak-forms + org.keycloak + 1.0-beta-1-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-email-freemarker + Keycloak Email FreeMarker + + + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-email-api + ${project.version} + provided + + + org.keycloak + keycloak-model-api + ${project.version} + provided + + + org.keycloak + keycloak-forms-common-freemarker + ${project.version} + provided + + + org.jboss.logging + jboss-logging + provided + + + org.freemarker + freemarker + provided + + + javax.mail + mail + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + 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 new file mode 100644 index 0000000000..9590a9cd41 --- /dev/null +++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java @@ -0,0 +1,140 @@ +package org.keycloak.email.freemarker; + +import org.jboss.logging.Logger; +import org.keycloak.email.EmailException; +import org.keycloak.email.EmailProvider; +import org.keycloak.freemarker.ExtendingThemeManager; +import org.keycloak.freemarker.FreeMarkerUtil; +import org.keycloak.freemarker.Theme; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.provider.ProviderSession; + +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * @author Stian Thorgersen + */ +public class FreeMarkerEmailProvider implements EmailProvider { + + private static final Logger log = Logger.getLogger(FreeMarkerEmailProvider.class); + + private ProviderSession session; + private RealmModel realm; + private UserModel user; + + public FreeMarkerEmailProvider(ProviderSession session) { + this.session = session; + } + + @Override + public EmailProvider setRealm(RealmModel realm) { + this.realm = realm; + return this; + } + + @Override + public EmailProvider setUser(UserModel user) { + this.user = user; + return this; + } + + @Override + public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException { + Map attributes = new HashMap(); + attributes.put("link", link); + attributes.put("linkExpiration", expirationInMinutes); + + send("passwordResetSubject", "password-reset.ftl", attributes); + } + + @Override + public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException { + Map attributes = new HashMap(); + attributes.put("link", link); + attributes.put("linkExpiration", expirationInMinutes); + + send("emailVerificationSubject", "email-verification.ftl", attributes); + } + + private void send(String subjectKey, String template, Map attributes) throws EmailException { + try { + ExtendingThemeManager themeManager = new ExtendingThemeManager(session); + Theme theme = themeManager.createTheme(realm.getAccountTheme(), Theme.Type.EMAIL); + + String subject = theme.getMessages().getProperty(subjectKey); + String body = FreeMarkerUtil.processTemplate(attributes, template, theme); + + send(subject, body); + } catch (Exception e) { + throw new EmailException("Failed to template email", e); + } + } + + + private void send(String subject, String body) throws EmailException { + try { + String address = user.getEmail(); + Map config = realm.getSmtpConfig(); + + Properties props = new Properties(); + props.setProperty("mail.smtp.host", config.get("host")); + + boolean auth = "true".equals(config.get("auth")); + boolean ssl = "true".equals(config.get("ssl")); + boolean starttls = "true".equals(config.get("starttls")); + + if (config.containsKey("port")) { + props.setProperty("mail.smtp.port", config.get("port")); + } + + if (auth) { + props.put("mail.smtp.auth", "true"); + } + + if (ssl) { + props.put("mail.smtp.socketFactory.port", config.get("port")); + props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + } + + if (starttls) { + props.put("mail.smtp.starttls.enable", "true"); + } + + String from = config.get("from"); + + Session session = Session.getInstance(props); + + Message msg = new MimeMessage(session); + msg.setFrom(new InternetAddress(from)); + msg.setHeader("To", address); + msg.setSubject(subject); + msg.setText(body); + msg.saveChanges(); + + Transport transport = session.getTransport("smtp"); + if (auth) { + transport.connect(config.get("user"), config.get("password")); + } else { + transport.connect(); + } + transport.sendMessage(msg, new InternetAddress[]{new InternetAddress(address)}); + } catch (Exception e) { + log.warn("Failed to send email", e); + throw new EmailException(e); + } + } + + @Override + public void close() { + } + +} diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProviderFactory.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProviderFactory.java new file mode 100644 index 0000000000..b98d9811fd --- /dev/null +++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProviderFactory.java @@ -0,0 +1,31 @@ +package org.keycloak.email.freemarker; + +import org.keycloak.Config; +import org.keycloak.email.EmailProvider; +import org.keycloak.email.EmailProviderFactory; +import org.keycloak.provider.ProviderSession; + +/** + * @author Stian Thorgersen + */ +public class FreeMarkerEmailProviderFactory implements EmailProviderFactory { + + @Override + public EmailProvider create(ProviderSession providerSession) { + return new FreeMarkerEmailProvider(providerSession); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "freemarker"; + } + +} diff --git a/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory b/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory new file mode 100644 index 0000000000..5b31f6e990 --- /dev/null +++ b/forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory @@ -0,0 +1 @@ +org.keycloak.email.freemarker.FreeMarkerEmailProviderFactory \ No newline at end of file diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java b/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java deleted file mode 100755 index a02df450b9..0000000000 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginForms.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.keycloak.login; - -import org.keycloak.models.ClientModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; - -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import java.util.List; - -/** - * @author Stian Thorgersen - */ -public interface LoginForms { - - public Response createResponse(UserModel.RequiredAction action); - - public Response createLogin(); - - public Response createPasswordReset(); - - public Response createLoginTotp(); - - public Response createRegistration(); - - public Response createErrorPage(); - - public Response createOAuthGrant(); - - public Response createCode(); - - public LoginForms setAccessCode(String accessCodeId, String accessCode); - - public LoginForms setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested); - - public LoginForms setError(String message); - - public LoginForms setSuccess(String message); - - public LoginForms setWarning(String message); - - public LoginForms setUser(UserModel user); - - public LoginForms setClient(ClientModel client); - - public LoginForms setQueryParams(MultivaluedMap queryParams); - - public LoginForms setFormData(MultivaluedMap formData); - - public LoginForms setStatus(Response.Status status); - -} diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsLoader.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsLoader.java deleted file mode 100644 index 817db88f1e..0000000000 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsLoader.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.keycloak.login; - -import java.util.ServiceLoader; - -/** - * @author Stian Thorgersen - */ -public class LoginFormsLoader { - - private LoginFormsLoader() { - } - - public static LoginFormsProvider load() { - return ServiceLoader.load(LoginFormsProvider.class).iterator().next(); - } - -} 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 be59cde25d..c686cf9796 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 @@ -1,14 +1,59 @@ -package org.keycloak.login; - -import org.keycloak.models.RealmModel; - -import javax.ws.rs.core.UriInfo; - -/** - * @author Stian Thorgersen - */ -public interface LoginFormsProvider { - - public LoginForms createForms(RealmModel realm, UriInfo uriInfo); - -} +package org.keycloak.login; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.provider.Provider; + +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public interface LoginFormsProvider extends Provider { + + public LoginFormsProvider setRealm(RealmModel realm); + + public LoginFormsProvider setUriInfo(UriInfo uriInfo); + + public Response createResponse(UserModel.RequiredAction action); + + public Response createLogin(); + + public Response createPasswordReset(); + + public Response createLoginTotp(); + + public Response createRegistration(); + + public Response createErrorPage(); + + public Response createOAuthGrant(); + + public Response createCode(); + + public LoginFormsProvider setAccessCode(String accessCodeId, String accessCode); + + public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested); + + public LoginFormsProvider setError(String message); + + public LoginFormsProvider setSuccess(String message); + + public LoginFormsProvider setWarning(String message); + + public LoginFormsProvider setUser(UserModel user); + + public LoginFormsProvider setClient(ClientModel client); + + public LoginFormsProvider setQueryParams(MultivaluedMap queryParams); + + public LoginFormsProvider setFormData(MultivaluedMap formData); + + public LoginFormsProvider setStatus(Response.Status status); + +} diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProviderFactory.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProviderFactory.java new file mode 100755 index 0000000000..0d9db65262 --- /dev/null +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProviderFactory.java @@ -0,0 +1,11 @@ +package org.keycloak.login; + +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.ProviderSession; + +/** + * @author Stian Thorgersen + */ +public interface LoginFormsProviderFactory extends ProviderFactory { + +} diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsSpi.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsSpi.java new file mode 100644 index 0000000000..e3fffeac73 --- /dev/null +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsSpi.java @@ -0,0 +1,25 @@ +package org.keycloak.login; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class LoginFormsSpi implements Spi { + @Override + public String getName() { + return "login-forms"; + } + + @Override + public Class getProviderClass() { + return LoginFormsProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return LoginFormsProviderFactory.class; + } +} diff --git a/forms/login-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/forms/login-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..afbdfbf299 --- /dev/null +++ b/forms/login-api/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.login.LoginFormsSpi \ No newline at end of file diff --git a/forms/login-freemarker/pom.xml b/forms/login-freemarker/pom.xml index c9e85bfa6e..9e512320c8 100755 --- a/forms/login-freemarker/pom.xml +++ b/forms/login-freemarker/pom.xml @@ -32,6 +32,12 @@ ${project.version} provided + + org.keycloak + keycloak-email-api + ${project.version} + provided + org.keycloak keycloak-model-api diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java deleted file mode 100755 index 50c68a778d..0000000000 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java +++ /dev/null @@ -1,287 +0,0 @@ -package org.keycloak.login.freemarker; - -import org.jboss.logging.Logger; -import org.keycloak.OAuth2Constants; -import org.keycloak.freemarker.FreeMarkerException; -import org.keycloak.freemarker.FreeMarkerUtil; -import org.keycloak.freemarker.Theme; -import org.keycloak.freemarker.ThemeLoader; -import org.keycloak.login.LoginForms; -import org.keycloak.login.LoginFormsPages; -import org.keycloak.login.freemarker.model.CodeBean; -import org.keycloak.login.freemarker.model.LoginBean; -import org.keycloak.login.freemarker.model.MessageBean; -import org.keycloak.login.freemarker.model.OAuthGrantBean; -import org.keycloak.login.freemarker.model.ProfileBean; -import org.keycloak.login.freemarker.model.RealmBean; -import org.keycloak.login.freemarker.model.RegisterBean; -import org.keycloak.login.freemarker.model.SocialBean; -import org.keycloak.login.freemarker.model.TotpBean; -import org.keycloak.login.freemarker.model.UrlBean; -import org.keycloak.models.ClientModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; -import org.keycloak.services.email.EmailException; -import org.keycloak.services.email.EmailSender; -import org.keycloak.services.messages.Messages; - -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 java.io.IOException; -import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -/** - * @author Stian Thorgersen - */ -public class FreeMarkerLoginForms implements LoginForms { - - private static final Logger logger = Logger.getLogger(FreeMarkerLoginForms.class); - - private String message; - private String accessCodeId; - private String accessCode; - private Response.Status status = Response.Status.OK; - private List realmRolesRequested; - private MultivaluedMap resourceRolesRequested; - private MultivaluedMap queryParams; - - public static enum MessageType {SUCCESS, WARNING, ERROR} - - private MessageType messageType = MessageType.ERROR; - - private MultivaluedMap formData; - - private RealmModel realm; - - private UserModel user; - - private ClientModel client; - - private UriInfo uriInfo; - - FreeMarkerLoginForms(RealmModel realm, UriInfo uriInfo) { - this.realm = realm; - this.uriInfo = uriInfo; - } - - public Response createResponse(UserModel.RequiredAction action) { - String actionMessage; - LoginFormsPages page; - - switch (action) { - case CONFIGURE_TOTP: - actionMessage = Messages.ACTION_WARN_TOTP; - page = LoginFormsPages.LOGIN_CONFIG_TOTP; - break; - case UPDATE_PROFILE: - actionMessage = Messages.ACTION_WARN_PROFILE; - page = LoginFormsPages.LOGIN_UPDATE_PROFILE; - break; - case UPDATE_PASSWORD: - actionMessage = Messages.ACTION_WARN_PASSWD; - page = LoginFormsPages.LOGIN_UPDATE_PASSWORD; - break; - case VERIFY_EMAIL: - try { - new EmailSender(realm.getSmtpConfig()).sendEmailVerification(user, realm, accessCodeId, uriInfo); - } catch (EmailException e) { - return setError("emailSendError").createErrorPage(); - } - - actionMessage = Messages.ACTION_WARN_EMAIL; - page = LoginFormsPages.LOGIN_VERIFY_EMAIL; - break; - default: - return Response.serverError().build(); - } - - if (message == null) { - setWarning(actionMessage); - } - - return createResponse(page); - } - - private Response createResponse(LoginFormsPages page) { - MultivaluedMap queryParameterMap = queryParams != null ? queryParams : uriInfo.getQueryParameters(); - - String requestURI = uriInfo.getBaseUri().getPath(); - UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); - - for (String k : queryParameterMap.keySet()) { - - Object[] objects = queryParameterMap.get(k).toArray(); - if (objects.length == 1 && objects[0] == null) continue; // - uriBuilder.replaceQueryParam(k, objects); - } - - if (accessCode != null) { - uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); - } - - Map attributes = new HashMap(); - - Theme theme; - try { - theme = ThemeLoader.createTheme(realm.getLoginTheme(), Theme.Type.LOGIN); - } catch (FreeMarkerException e) { - logger.error("Failed to create theme", e); - return Response.serverError().build(); - } - - try { - attributes.put("properties", theme.getProperties()); - } catch (IOException e) { - logger.warn("Failed to load properties", e); - } - - Properties messages; - try { - messages = theme.getMessages(); - attributes.put("rb", messages); - } catch (IOException e) { - logger.warn("Failed to load messages", e); - messages = new Properties(); - } - - if (message != null) { - attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, 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 - uriBuilder.replaceQuery(null); - } - URI baseUri = uriBuilder.build(); - - if (realm != null) { - attributes.put("realm", new RealmBean(realm)); - attributes.put("social", new SocialBean(realm, baseUri)); - attributes.put("url", new UrlBean(realm, theme, baseUri)); - } - - attributes.put("login", new LoginBean(formData)); - - switch (page) { - case LOGIN_CONFIG_TOTP: - attributes.put("totp", new TotpBean(realm, user, baseUri)); - break; - case LOGIN_UPDATE_PROFILE: - attributes.put("user", new ProfileBean(user)); - break; - case REGISTER: - attributes.put("register", new RegisterBean(formData)); - break; - case OAUTH_GRANT: - attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested)); - break; - case CODE: - attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null)); - break; - } - - try { - String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme); - return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build(); - } catch (FreeMarkerException e) { - logger.error("Failed to process template", e); - return Response.serverError().build(); - } - } - - public Response createLogin() { - return createResponse(LoginFormsPages.LOGIN); - } - - public Response createPasswordReset() { - return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD); - } - - public Response createLoginTotp() { - return createResponse(LoginFormsPages.LOGIN_TOTP); - } - - public Response createRegistration() { - return createResponse(LoginFormsPages.REGISTER); - } - - public Response createErrorPage() { - setStatus(Response.Status.INTERNAL_SERVER_ERROR); - return createResponse(LoginFormsPages.ERROR); - } - - public Response createOAuthGrant() { - return createResponse(LoginFormsPages.OAUTH_GRANT); - } - - @Override - public Response createCode() { - return createResponse(LoginFormsPages.CODE); - } - - public FreeMarkerLoginForms setError(String message) { - this.message = message; - this.messageType = MessageType.ERROR; - return this; - } - - public FreeMarkerLoginForms setSuccess(String message) { - this.message = message; - this.messageType = MessageType.SUCCESS; - return this; - } - - public FreeMarkerLoginForms setWarning(String message) { - this.message = message; - this.messageType = MessageType.WARNING; - return this; - } - - public FreeMarkerLoginForms setUser(UserModel user) { - this.user = user; - return this; - } - - public FreeMarkerLoginForms setClient(ClientModel client) { - this.client = client; - return this; - } - - public FreeMarkerLoginForms setFormData(MultivaluedMap formData) { - this.formData = formData; - return this; - } - - @Override - public LoginForms setAccessCode(String accessCodeId, String accessCode) { - this.accessCodeId = accessCodeId; - this.accessCode = accessCode; - return this; - } - - @Override - public LoginForms setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested) { - this.realmRolesRequested = realmRolesRequested; - this.resourceRolesRequested = resourceRolesRequested; - return this; - } - - @Override - public LoginForms setStatus(Response.Status status) { - this.status = status; - return this; - } - - @Override - public LoginForms setQueryParams(MultivaluedMap queryParams) { - this.queryParams = queryParams; - return this; - } -} 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 5f0b0c50bc..1dfcd29827 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 @@ -1,19 +1,314 @@ -package org.keycloak.login.freemarker; - -import org.keycloak.login.LoginForms; -import org.keycloak.login.LoginFormsProvider; -import org.keycloak.models.RealmModel; - -import javax.ws.rs.core.UriInfo; - -/** - * @author Stian Thorgersen - */ -public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { - - @Override - public LoginForms createForms(RealmModel realm, UriInfo uriInfo) { - return new FreeMarkerLoginForms(realm, uriInfo); - } - -} +package org.keycloak.login.freemarker; + +import org.jboss.logging.Logger; +import org.keycloak.OAuth2Constants; +import org.keycloak.email.EmailException; +import org.keycloak.email.EmailProvider; +import org.keycloak.freemarker.ExtendingThemeManager; +import org.keycloak.freemarker.FreeMarkerException; +import org.keycloak.freemarker.FreeMarkerUtil; +import org.keycloak.freemarker.Theme; +import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.login.LoginFormsPages; +import org.keycloak.login.freemarker.model.CodeBean; +import org.keycloak.login.freemarker.model.LoginBean; +import org.keycloak.login.freemarker.model.MessageBean; +import org.keycloak.login.freemarker.model.OAuthGrantBean; +import org.keycloak.login.freemarker.model.ProfileBean; +import org.keycloak.login.freemarker.model.RealmBean; +import org.keycloak.login.freemarker.model.RegisterBean; +import org.keycloak.login.freemarker.model.SocialBean; +import org.keycloak.login.freemarker.model.TotpBean; +import org.keycloak.login.freemarker.model.UrlBean; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.provider.ProviderSession; +import org.keycloak.services.messages.Messages; +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 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.util.concurrent.TimeUnit; + +/** + * @author Stian Thorgersen + */ +public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { + + private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class); + + private String message; + private String accessCodeId; + private String accessCode; + private Response.Status status = Response.Status.OK; + private List realmRolesRequested; + private MultivaluedMap resourceRolesRequested; + private MultivaluedMap queryParams; + + public static enum MessageType {SUCCESS, WARNING, ERROR} + + private MessageType messageType = MessageType.ERROR; + + private MultivaluedMap formData; + + private ProviderSession session; + private RealmModel realm; + + private UserModel user; + + private ClientModel client; + + private UriInfo uriInfo; + + public FreeMarkerLoginFormsProvider(ProviderSession session) { + this.session = session; + } + + public LoginFormsProvider setRealm(RealmModel realm) { + this.realm = realm; + return this; + } + + public LoginFormsProvider setUriInfo(UriInfo uriInfo) { + this.uriInfo = uriInfo; + return this; + } + + public Response createResponse(UserModel.RequiredAction action) { + String actionMessage; + LoginFormsPages page; + + switch (action) { + case CONFIGURE_TOTP: + actionMessage = Messages.ACTION_WARN_TOTP; + page = LoginFormsPages.LOGIN_CONFIG_TOTP; + break; + case UPDATE_PROFILE: + actionMessage = Messages.ACTION_WARN_PROFILE; + page = LoginFormsPages.LOGIN_UPDATE_PROFILE; + break; + case UPDATE_PASSWORD: + actionMessage = Messages.ACTION_WARN_PASSWD; + page = LoginFormsPages.LOGIN_UPDATE_PASSWORD; + break; + case VERIFY_EMAIL: + try { + UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); + builder.queryParam("key", accessCodeId); + + String link = builder.build(realm.getName()).toString(); + long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); + + 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(); + } + + actionMessage = Messages.ACTION_WARN_EMAIL; + page = LoginFormsPages.LOGIN_VERIFY_EMAIL; + break; + default: + return Response.serverError().build(); + } + + if (message == null) { + setWarning(actionMessage); + } + + return createResponse(page); + } + + private Response createResponse(LoginFormsPages page) { + MultivaluedMap queryParameterMap = queryParams != null ? queryParams : uriInfo.getQueryParameters(); + + String requestURI = uriInfo.getBaseUri().getPath(); + UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); + + for (String k : queryParameterMap.keySet()) { + + Object[] objects = queryParameterMap.get(k).toArray(); + if (objects.length == 1 && objects[0] == null) continue; // + uriBuilder.replaceQueryParam(k, objects); + } + + if (accessCode != null) { + uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); + } + + Map attributes = new HashMap(); + + ExtendingThemeManager themeManager = new ExtendingThemeManager(session); + Theme theme; + try { + theme = themeManager.createTheme(realm.getLoginTheme(), Theme.Type.LOGIN); + } catch (IOException e) { + logger.error("Failed to create theme", e); + return Response.serverError().build(); + } + + try { + attributes.put("properties", theme.getProperties()); + } catch (IOException e) { + logger.warn("Failed to load properties", e); + } + + Properties messages; + try { + messages = theme.getMessages(); + attributes.put("rb", messages); + } catch (IOException e) { + logger.warn("Failed to load messages", e); + messages = new Properties(); + } + + if (message != null) { + attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, 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 + uriBuilder.replaceQuery(null); + } + URI baseUri = uriBuilder.build(); + + if (realm != null) { + attributes.put("realm", new RealmBean(realm)); + attributes.put("social", new SocialBean(realm, baseUri)); + attributes.put("url", new UrlBean(realm, theme, baseUri)); + } + + attributes.put("login", new LoginBean(formData)); + + switch (page) { + case LOGIN_CONFIG_TOTP: + attributes.put("totp", new TotpBean(realm, user, baseUri)); + break; + case LOGIN_UPDATE_PROFILE: + attributes.put("user", new ProfileBean(user)); + break; + case REGISTER: + attributes.put("register", new RegisterBean(formData)); + break; + case OAUTH_GRANT: + attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested)); + break; + case CODE: + attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null)); + break; + } + + try { + String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme); + return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build(); + } catch (FreeMarkerException e) { + logger.error("Failed to process template", e); + return Response.serverError().build(); + } + } + + public Response createLogin() { + return createResponse(LoginFormsPages.LOGIN); + } + + public Response createPasswordReset() { + return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD); + } + + public Response createLoginTotp() { + return createResponse(LoginFormsPages.LOGIN_TOTP); + } + + public Response createRegistration() { + return createResponse(LoginFormsPages.REGISTER); + } + + public Response createErrorPage() { + setStatus(Response.Status.INTERNAL_SERVER_ERROR); + return createResponse(LoginFormsPages.ERROR); + } + + public Response createOAuthGrant() { + return createResponse(LoginFormsPages.OAUTH_GRANT); + } + + @Override + public Response createCode() { + return createResponse(LoginFormsPages.CODE); + } + + public FreeMarkerLoginFormsProvider setError(String message) { + this.message = message; + this.messageType = MessageType.ERROR; + return this; + } + + public FreeMarkerLoginFormsProvider setSuccess(String message) { + this.message = message; + this.messageType = MessageType.SUCCESS; + return this; + } + + public FreeMarkerLoginFormsProvider setWarning(String message) { + this.message = message; + this.messageType = MessageType.WARNING; + return this; + } + + public FreeMarkerLoginFormsProvider setUser(UserModel user) { + this.user = user; + return this; + } + + public FreeMarkerLoginFormsProvider setClient(ClientModel client) { + this.client = client; + return this; + } + + public FreeMarkerLoginFormsProvider setFormData(MultivaluedMap formData) { + this.formData = formData; + return this; + } + + @Override + public LoginFormsProvider setAccessCode(String accessCodeId, String accessCode) { + this.accessCodeId = accessCodeId; + this.accessCode = accessCode; + return this; + } + + @Override + public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested) { + this.realmRolesRequested = realmRolesRequested; + this.resourceRolesRequested = resourceRolesRequested; + return this; + } + + @Override + public LoginFormsProvider setStatus(Response.Status status) { + this.status = status; + return this; + } + + @Override + public LoginFormsProvider setQueryParams(MultivaluedMap queryParams) { + this.queryParams = queryParams; + return this; + } + + @Override + public void close() { + } + +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProviderFactory.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProviderFactory.java new file mode 100755 index 0000000000..6b11d19e23 --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProviderFactory.java @@ -0,0 +1,31 @@ +package org.keycloak.login.freemarker; + +import org.keycloak.Config; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.login.LoginFormsProviderFactory; +import org.keycloak.provider.ProviderSession; + +/** + * @author Stian Thorgersen + */ +public class FreeMarkerLoginFormsProviderFactory implements LoginFormsProviderFactory { + + @Override + public LoginFormsProvider create(ProviderSession providerSession) { + return new FreeMarkerLoginFormsProvider(providerSession); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "freemarker"; + } + +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java index a6b36d4678..72d48b7d88 100644 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/MessageBean.java @@ -21,7 +21,7 @@ */ package org.keycloak.login.freemarker.model; -import org.keycloak.login.freemarker.FreeMarkerLoginForms; +import org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider; /** * @author Stian Thorgersen @@ -30,9 +30,9 @@ public class MessageBean { private String summary; - private FreeMarkerLoginForms.MessageType type; + private FreeMarkerLoginFormsProvider.MessageType type; - public MessageBean(String message, FreeMarkerLoginForms.MessageType type) { + public MessageBean(String message, FreeMarkerLoginFormsProvider.MessageType type) { this.summary = message; this.type = type; } @@ -46,15 +46,15 @@ public class MessageBean { } public boolean isSuccess() { - return FreeMarkerLoginForms.MessageType.SUCCESS.equals(this.type); + return FreeMarkerLoginFormsProvider.MessageType.SUCCESS.equals(this.type); } public boolean isWarning() { - return FreeMarkerLoginForms.MessageType.WARNING.equals(this.type); + return FreeMarkerLoginFormsProvider.MessageType.WARNING.equals(this.type); } public boolean isError() { - return FreeMarkerLoginForms.MessageType.ERROR.equals(this.type); + return FreeMarkerLoginFormsProvider.MessageType.ERROR.equals(this.type); } } \ No newline at end of file diff --git a/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider deleted file mode 100644 index ae28fdb717..0000000000 --- a/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider +++ /dev/null @@ -1 +0,0 @@ -org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider \ No newline at end of file diff --git a/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory new file mode 100644 index 0000000000..893783b0d8 --- /dev/null +++ b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory @@ -0,0 +1 @@ +org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory \ No newline at end of file diff --git a/forms/pom.xml b/forms/pom.xml index f70ac61dcb..0f2268dbb5 100755 --- a/forms/pom.xml +++ b/forms/pom.xml @@ -19,6 +19,8 @@ common-themes account-api account-freemarker + email-api + email-freemarker login-api login-freemarker diff --git a/pom.xml b/pom.xml index 335383bd42..0aa3dff11e 100755 --- a/pom.xml +++ b/pom.xml @@ -121,6 +121,11 @@ base64 2.3.8 + + javax.mail + mail + 1.4.7 + org.jboss.resteasy jaxrs-api diff --git a/project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java b/project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java index d8ef5e0906..650af915ae 100755 --- a/project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java +++ b/project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java @@ -59,4 +59,8 @@ public class AerogearThemeProvider implements ThemeProvider { return nameSet(type).contains(name); } + @Override + public void close() { + } + } diff --git a/server/pom.xml b/server/pom.xml index 1276b48d31..e353dd598e 100755 --- a/server/pom.xml +++ b/server/pom.xml @@ -117,6 +117,16 @@ keycloak-account-freemarker ${project.version} + + org.keycloak + keycloak-email-api + ${project.version} + + + org.keycloak + keycloak-email-freemarker + ${project.version} + org.keycloak keycloak-login-api diff --git a/server/src/main/resources/META-INF/keycloak-server.json b/server/src/main/resources/META-INF/keycloak-server.json index 62405d9d43..544ad8147c 100644 --- a/server/src/main/resources/META-INF/keycloak-server.json +++ b/server/src/main/resources/META-INF/keycloak-server.json @@ -20,6 +20,18 @@ "dir": "${jboss.server.config.dir}/themes" }, + "login-forms": { + "provider": "freemarker" + }, + + "account": { + "provider": "freemarker" + }, + + "email": { + "provider": "freemarker" + }, + "scheduled": { "interval": 900 } diff --git a/services/pom.xml b/services/pom.xml index 6b8ceef248..7612a20e86 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -49,6 +49,12 @@ ${project.version} provided + + org.keycloak + keycloak-email-api + ${project.version} + provided + org.keycloak keycloak-login-api diff --git a/services/src/main/java/org/keycloak/services/email/EmailSender.java b/services/src/main/java/org/keycloak/services/email/EmailSender.java deleted file mode 100755 index 6b8af21e08..0000000000 --- a/services/src/main/java/org/keycloak/services/email/EmailSender.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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.services.email; - -import org.jboss.logging.Logger; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.services.managers.AccessCodeEntry; -import org.keycloak.services.resources.flows.Urls; - -import javax.mail.Message; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; -import java.net.URI; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.TimeUnit; - -/** - * @author Stian Thorgersen - */ -public class EmailSender { - - private static final Logger log = Logger.getLogger(EmailSender.class); - - private Map config; - - public EmailSender(Map config) { - this.config = config; - } - - public void send(String address, String subject, String body) throws EmailException { - try { - Properties props = new Properties(); - props.setProperty("mail.smtp.host", config.get("host")); - - boolean auth = "true".equals(config.get("auth")); - boolean ssl = "true".equals(config.get("ssl")); - boolean starttls = "true".equals(config.get("starttls")); - - if (config.containsKey("port")) { - props.setProperty("mail.smtp.port", config.get("port")); - } - - if (auth) { - props.put("mail.smtp.auth", "true"); - } - - if (ssl) { - props.put("mail.smtp.socketFactory.port", config.get("port")); - props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); - } - - if (starttls) { - props.put("mail.smtp.starttls.enable", "true"); - } - - String from = config.get("from"); - - Session session = Session.getInstance(props); - - Message msg = new MimeMessage(session); - msg.setFrom(new InternetAddress(from)); - msg.setHeader("To", address); - msg.setSubject(subject); - msg.setText(body); - msg.saveChanges(); - - Transport transport = session.getTransport("smtp"); - if (auth) { - transport.connect(config.get("user"), config.get("password")); - } else { - transport.connect(); - } - transport.sendMessage(msg, new InternetAddress[]{new InternetAddress(address)}); - } catch (Exception e) { - log.warn("Failed to send email", e); - throw new EmailException(e); - } - } - - public void sendEmailVerification(UserModel user, RealmModel realm, String accessCodeId, UriInfo uriInfo) throws EmailException { - UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCodeId); - - URI uri = builder.build(realm.getName()); - - - StringBuilder sb = getHeader(user); - - sb.append("Someone has created a Keycloak account with this email address. "); - sb.append("If this was you, click the link below to verify your email address:\n"); - sb.append(uri.toString()); - sb.append("\n\nThis link will expire within ").append(TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction())); - sb.append(" minutes.\n\n"); - sb.append("If you didn't create this account, just ignore this message.\n"); - - addFooter(sb); - - send(user.getEmail(), "Verify email", sb.toString()); - } - - public void sendPasswordReset(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) throws EmailException { - UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCode.getId()); - - URI uri = builder.build(realm.getName()); - - StringBuilder sb = getHeader(user); - - sb.append("Someone just requested to change your Keycloak account's password. "); - sb.append("If this was you, click on the link below to set a new password:\n"); - sb.append(uri.toString()); - sb.append("\n\nThis link will expire within ").append(TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction())); - sb.append(" minutes.\n\n"); - sb.append("If you don't want to reset your password, just ignore this message and nothing will be changed.\n"); - - addFooter(sb); - - send(user.getEmail(), "Reset password link", sb.toString()); - } - - private StringBuilder getHeader(UserModel user) { - StringBuilder sb = new StringBuilder(); - - sb.append("Hi"); - if (user.getFirstName() != null) { - sb.append(" ").append(user.getFirstName()); - } - sb.append(",\n\n"); - return sb; - } - - private void addFooter(StringBuilder sb) { - sb.append("\nThanks,\nThe Keycloak Team"); - } - - -} 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 453f550a40..c23858da48 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -347,7 +347,7 @@ public class AuthenticationManager { private boolean checkEnabled(UserModel user) { if (!user.isEnabled()) { - logger.warn("Account is disabled, contact admin. " + user.getLoginName()); + logger.warn("AccountProvider is disabled, contact admin. " + user.getLoginName()); return false; } else { return true; 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 85a1461626..5f6206ffb4 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -25,14 +25,16 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.OAuth2Constants; -import org.keycloak.account.Account; -import org.keycloak.account.AccountLoader; import org.keycloak.account.AccountPages; +import org.keycloak.account.AccountProvider; import org.keycloak.audit.Audit; import org.keycloak.audit.AuditProvider; import org.keycloak.audit.Details; import org.keycloak.audit.Event; import org.keycloak.audit.Events; +import org.keycloak.authentication.AuthProviderStatus; +import org.keycloak.authentication.AuthenticationProviderException; +import org.keycloak.authentication.AuthenticationProviderManager; import org.keycloak.models.AccountRoles; import org.keycloak.models.ApplicationModel; import org.keycloak.models.AuthenticationLinkModel; @@ -44,8 +46,8 @@ import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.TimeBasedOTP; -import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.provider.ProviderSession; +import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.ForbiddenException; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; @@ -62,9 +64,6 @@ import org.keycloak.services.validation.Validation; import org.keycloak.social.SocialLoader; import org.keycloak.social.SocialProvider; import org.keycloak.social.SocialProviderException; -import org.keycloak.authentication.AuthProviderStatus; -import org.keycloak.authentication.AuthenticationProviderException; -import org.keycloak.authentication.AuthenticationProviderManager; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -76,7 +75,6 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; @@ -111,8 +109,6 @@ public class AccountService { AUDIT_DETAILS.add(Details.AUTH_METHOD); } - public static final String KEYCLOAK_ACCOUNT_IDENTITY_COOKIE = "KEYCLOAK_ACCOUNT_IDENTITY"; - private RealmModel realm; @Context @@ -131,7 +127,7 @@ public class AccountService { private final ApplicationModel application; private Audit audit; private final SocialRequestManager socialRequestManager; - private Account account; + private AccountProvider account; private Auth auth; private AuditProvider auditProvider; @@ -146,7 +142,7 @@ public class AccountService { public void init() { auditProvider = providers.getProvider(AuditProvider.class); - account = AccountLoader.load().createAccount(uriInfo).setRealm(realm); + account = providers.getProvider(AccountProvider.class).setRealm(realm).setUriInfo(uriInfo); boolean passwordUpdateSupported = false; AuthenticationManager.AuthResult authResult = authManager.authenticateRequest(realm, uriInfo, headers); @@ -181,7 +177,7 @@ public class AccountService { try { require(AccountRoles.MANAGE_ACCOUNT); } catch (ForbiddenException e) { - return Flows.forms(realm, uriInfo).setError("No access").createErrorPage(); + return Flows.forms(providers, realm, uriInfo).setError("No access").createErrorPage(); } String[] referrer = getReferrer(); diff --git a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java index 4c6139cad1..4c1e06218e 100755 --- a/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java @@ -28,7 +28,9 @@ import org.keycloak.audit.Audit; import org.keycloak.audit.Details; import org.keycloak.audit.Errors; import org.keycloak.audit.Events; -import org.keycloak.login.LoginForms; +import org.keycloak.email.EmailException; +import org.keycloak.email.EmailProvider; +import org.keycloak.login.LoginFormsProvider; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.ClientModel; @@ -41,13 +43,12 @@ import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.provider.ProviderSession; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.ClientConnection; -import org.keycloak.services.email.EmailException; -import org.keycloak.services.email.EmailSender; import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.flows.Flows; +import org.keycloak.services.resources.flows.Urls; import org.keycloak.services.validation.Validation; import org.keycloak.authentication.AuthenticationProviderException; import org.keycloak.authentication.AuthenticationProviderManager; @@ -62,12 +63,12 @@ import javax.ws.rs.core.HttpHeaders; 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.ext.Providers; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * @author Stian Thorgersen @@ -120,7 +121,7 @@ public class RequiredActionsService { String error = Validation.validateUpdateProfileForm(formData); if (error != null) { - return Flows.forms(realm, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE); + return Flows.forms(providerSession, realm, uriInfo).setUser(user).setError(error).createResponse(RequiredAction.UPDATE_PROFILE); } user.setFirstName(formData.getFirst("firstName")); @@ -160,7 +161,7 @@ public class RequiredActionsService { String totp = formData.getFirst("totp"); String totpSecret = formData.getFirst("totpSecret"); - LoginForms loginForms = Flows.forms(realm, uriInfo).setUser(user); + LoginFormsProvider loginForms = Flows.forms(providerSession, realm, uriInfo).setUser(user); if (Validation.isEmpty(totp)) { return loginForms.setError(Messages.MISSING_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP); } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { @@ -201,7 +202,7 @@ public class RequiredActionsService { String passwordNew = formData.getFirst("password-new"); String passwordConfirm = formData.getFirst("password-confirm"); - LoginForms loginForms = Flows.forms(realm, uriInfo).setUser(user); + LoginFormsProvider loginForms = Flows.forms(providerSession, realm, uriInfo).setUser(user); if (Validation.isEmpty(passwordNew)) { return loginForms.setError(Messages.MISSING_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD); } else if (!passwordNew.equals(passwordConfirm)) { @@ -261,7 +262,7 @@ public class RequiredActionsService { initAudit(accessCode); //audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); - return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser()) + return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(accessCode.getUser()) .createResponse(RequiredAction.VERIFY_EMAIL); } } @@ -277,9 +278,9 @@ public class RequiredActionsService { return unauthorized(); } - return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD); + return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).createResponse(RequiredAction.UPDATE_PASSWORD); } else { - return Flows.forms(realm, uriInfo).createPasswordReset(); + return Flows.forms(providerSession, realm, uriInfo).createPasswordReset(); } } @@ -298,11 +299,11 @@ public class RequiredActionsService { ClientModel client = realm.findClient(clientId); if (client == null) { - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure( + return Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure( "Unknown login requester."); } if (!client.isEnabled()) { - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure( + return Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager).forwardToSecurityFailure( "Login requester not enabled."); } @@ -334,15 +335,22 @@ public class RequiredActionsService { accessCode.setUsername(username); try { - new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo); + UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); + builder.queryParam("key", accessCode.getId()); + + String link = builder.build(realm.getName()).toString(); + long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); + + providerSession.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration); + audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getId()).success(); } catch (EmailException e) { logger.error("Failed to send password reset email", e); - return Flows.forms(realm, uriInfo).setError("emailSendError").createErrorPage(); + return Flows.forms(providerSession, realm, uriInfo).setError("emailSendError").createErrorPage(); } } - return Flows.forms(realm, uriInfo).setSuccess("emailSent").createPasswordReset(); + return Flows.forms(providerSession, realm, uriInfo).setSuccess("emailSent").createPasswordReset(); } private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) { @@ -399,7 +407,7 @@ public class RequiredActionsService { Set requiredActions = user.getRequiredActions(); if (!requiredActions.isEmpty()) { - return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user) + return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user) .createResponse(requiredActions.iterator().next()); } else { logger.debugv("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri()); @@ -410,13 +418,13 @@ public class RequiredActionsService { UserSessionModel session = realm.getUserSession(accessCode.getSessionState()); if (!AuthenticationManager.isSessionValid(realm, session)) { AuthenticationManager.logout(realm, session, uriInfo); - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri()); + return Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri()); } audit.session(session); audit.success(); - return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, + return Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager).redirectAccessCode(accessCode, session, accessCode.getState(), accessCode.getRedirectUri()); } } @@ -437,7 +445,7 @@ public class RequiredActionsService { } private Response unauthorized() { - return Flows.forms(realm, uriInfo).setError("Unauthorized request").createErrorPage(); + return Flows.forms(providerSession, realm, uriInfo).setError("Unauthorized request").createErrorPage(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/SocialResource.java b/services/src/main/java/org/keycloak/services/resources/SocialResource.java index 06f0ae60af..76c898511c 100755 --- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java +++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java @@ -96,6 +96,9 @@ public class SocialResource { ResourceContext resourceContext; */ + @Context + protected ProviderSession providerSession; + @Context protected KeycloakSession session; @@ -133,7 +136,7 @@ public class SocialResource { .detail(Details.AUTH_METHOD, "social@" + provider.getId()); AuthenticationManager authManager = new AuthenticationManager(providers); - OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); + OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager); if (!realm.isEnabled()) { audit.error(Errors.REALM_DISABLED); @@ -177,7 +180,7 @@ public class SocialResource { queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, responseType); audit.error(Errors.REJECTED_BY_USER); - return Flows.forms(realm, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin(); + return Flows.forms(providerSession, realm, uriInfo).setQueryParams(queryParms).setWarning("Access denied").createLogin(); } catch (SocialProviderException e) { logger.error("Failed to process social callback", e); return oauth.forwardToSecurityFailure("Failed to process social callback"); @@ -279,25 +282,25 @@ public class SocialResource { SocialProvider provider = SocialLoader.load(providerId); if (provider == null) { audit.error(Errors.SOCIAL_PROVIDER_NOT_FOUND); - return Flows.forms(realm, uriInfo).setError("Social provider not found").createErrorPage(); + return Flows.forms(providerSession, realm, uriInfo).setError("Social provider not found").createErrorPage(); } ClientModel client = realm.findClient(clientId); if (client == null) { audit.error(Errors.CLIENT_NOT_FOUND); logger.warn("Unknown login requester: " + clientId); - return Flows.forms(realm, uriInfo).setError("Unknown login requester.").createErrorPage(); + return Flows.forms(providerSession, realm, uriInfo).setError("Unknown login requester.").createErrorPage(); } if (!client.isEnabled()) { audit.error(Errors.CLIENT_DISABLED); logger.warn("Login requester not enabled."); - return Flows.forms(realm, uriInfo).setError("Login requester not enabled.").createErrorPage(); + return Flows.forms(providerSession, realm, uriInfo).setError("Login requester not enabled.").createErrorPage(); } redirectUri = TokenService.verifyRedirectUri(uriInfo, redirectUri, client); if (redirectUri == null) { audit.error(Errors.INVALID_REDIRECT_URI); - return Flows.forms(realm, uriInfo).setError("Invalid redirect_uri.").createErrorPage(); + return Flows.forms(providerSession, realm, uriInfo).setError("Invalid redirect_uri.").createErrorPage(); } try { @@ -308,7 +311,7 @@ public class SocialResource { .putClientAttribute("responseType", responseType).redirectToSocialProvider(); } catch (Throwable t) { logger.error("Failed to redirect to social auth", t); - return Flows.forms(realm, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); + return Flows.forms(providerSession, realm, uriInfo).setError("Failed to redirect to social auth").createErrorPage(); } } diff --git a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java index cc04a571b5..1d4fe1c499 100755 --- a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java @@ -1,14 +1,17 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; +import org.keycloak.freemarker.ExtendingThemeManager; import org.keycloak.freemarker.Theme; -import org.keycloak.freemarker.ThemeLoader; +import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.provider.ProviderSession; import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import java.io.InputStream; @@ -22,11 +25,15 @@ public class ThemeResource { private static FileTypeMap mimeTypes = MimetypesFileTypeMap.getDefaultFileTypeMap(); + @Context + private ProviderSession providerSession; + @GET @Path("/{themType}/{themeName}/{path:.*}") public Response getResource(@PathParam("themType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) { try { - Theme theme = ThemeLoader.createTheme(themeName, Theme.Type.valueOf(themType.toUpperCase())); + ExtendingThemeManager themeManager = new ExtendingThemeManager(providerSession); + Theme theme = themeManager.createTheme(themeName, Theme.Type.valueOf(themType.toUpperCase())); InputStream resource = theme.getResourceAsStream(path); if (resource != null) { return Response.ok(resource).type(mimeTypes.getContentType(path)).build(); @@ -39,5 +46,4 @@ public class ThemeResource { } } - } diff --git a/services/src/main/java/org/keycloak/services/resources/TokenService.java b/services/src/main/java/org/keycloak/services/resources/TokenService.java index d9c12de26f..9b47de5002 100755 --- a/services/src/main/java/org/keycloak/services/resources/TokenService.java +++ b/services/src/main/java/org/keycloak/services/resources/TokenService.java @@ -242,14 +242,14 @@ public class TokenService { case ACTIONS_REQUIRED: err = new HashMap(); err.put(OAuth2Constants.ERROR, "invalid_grant"); - err.put(OAuth2Constants.ERROR_DESCRIPTION, "Account temporarily disabled"); + err.put(OAuth2Constants.ERROR_DESCRIPTION, "AccountProvider temporarily disabled"); audit.error(Errors.USER_TEMPORARILY_DISABLED); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) .build(); case ACCOUNT_DISABLED: err = new HashMap(); err.put(OAuth2Constants.ERROR, "invalid_grant"); - err.put(OAuth2Constants.ERROR_DESCRIPTION, "Account disabled"); + err.put(OAuth2Constants.ERROR_DESCRIPTION, "AccountProvider disabled"); audit.error(Errors.USER_DISABLED); return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) .build(); @@ -340,7 +340,7 @@ public class TokenService { audit.detail(Details.REMEMBER_ME, "true"); } - OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); + OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager); if (!checkSsl()) { return oauth.forwardToSecurityFailure("HTTPS required"); @@ -391,18 +391,18 @@ public class TokenService { return oauth.processAccessCode(scopeParam, state, redirect, client, user, session, username, remember, "form", audit); case ACCOUNT_TEMPORARILY_DISABLED: audit.error(Errors.USER_TEMPORARILY_DISABLED); - return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin(); + return Flows.forms(providerSession, realm, uriInfo).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).setFormData(formData).createLogin(); case ACCOUNT_DISABLED: audit.error(Errors.USER_DISABLED); - return Flows.forms(realm, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); + return Flows.forms(providerSession, realm, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); case MISSING_TOTP: - return Flows.forms(realm, uriInfo).setFormData(formData).createLoginTotp(); + return Flows.forms(providerSession, realm, uriInfo).setFormData(formData).createLoginTotp(); case INVALID_USER: audit.error(Errors.USER_NOT_FOUND); - return Flows.forms(realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); + return Flows.forms(providerSession, realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); default: audit.error(Errors.INVALID_USER_CREDENTIALS); - return Flows.forms(realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); + return Flows.forms(providerSession, realm, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); } } @@ -432,7 +432,7 @@ public class TokenService { .detail(Details.EMAIL, email) .detail(Details.REGISTER_METHOD, "form"); - OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); + OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager); if (!realm.isEnabled()) { logger.warn("Realm not enabled"); @@ -477,7 +477,7 @@ public class TokenService { if (error != null) { audit.error(Errors.INVALID_REGISTRATION); - return Flows.forms(realm, uriInfo).setError(error).setFormData(formData).createRegistration(); + return Flows.forms(providerSession, realm, uriInfo).setError(error).setFormData(formData).createRegistration(); } AuthenticationProviderManager authenticationProviderManager = AuthenticationProviderManager.getManager(realm, providerSession); @@ -485,7 +485,7 @@ public class TokenService { // Validate that user with this username doesn't exist in realm or any authentication provider if (realm.getUser(username) != null || authenticationProviderManager.getUser(username) != null) { audit.error(Errors.USERNAME_IN_USE); - return Flows.forms(realm, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); + return Flows.forms(providerSession, realm, uriInfo).setError(Messages.USERNAME_EXISTS).setFormData(formData).createRegistration(); } UserModel user = realm.addUser(username); @@ -513,7 +513,7 @@ public class TokenService { // User already registered, but force him to update password if (!passwordUpdateSuccessful) { user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); - return Flows.forms(realm, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); + return Flows.forms(providerSession, realm, uriInfo).setError(passwordUpdateError).createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); } } @@ -722,7 +722,7 @@ public class TokenService { audit.event(Events.LOGIN).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); - OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); + OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager); if (!checkSsl()) { return oauth.forwardToSecurityFailure("HTTPS required"); @@ -766,7 +766,7 @@ public class TokenService { return oauth.redirectError(client, "access_denied", state, redirect); } logger.info("createLogin() now..."); - return Flows.forms(realm, uriInfo).createLogin(); + return Flows.forms(providerSession, realm, uriInfo).createLogin(); } @Path("registrations") @@ -778,7 +778,7 @@ public class TokenService { audit.event(Events.REGISTER).client(clientId).detail(Details.REDIRECT_URI, redirect).detail(Details.RESPONSE_TYPE, "code"); - OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); + OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager); if (!checkSsl()) { return oauth.forwardToSecurityFailure("HTTPS required"); @@ -816,7 +816,7 @@ public class TokenService { authManager.expireIdentityCookie(realm, uriInfo); - return Flows.forms(realm, uriInfo).createRegistration(); + return Flows.forms(providerSession, realm, uriInfo).createRegistration(); } @Path("logout") @@ -868,7 +868,7 @@ public class TokenService { public Response processOAuth(final MultivaluedMap formData) { audit.event(Events.LOGIN).detail(Details.RESPONSE_TYPE, "code"); - OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); + OAuthFlows oauth = Flows.oauth(providerSession, realm, request, uriInfo, authManager, tokenManager); if (!checkSsl()) { return oauth.forwardToSecurityFailure("HTTPS required"); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index eaf766640f..bd897fa340 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -6,8 +6,9 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.freemarker.ExtendingThemeManager; import org.keycloak.freemarker.Theme; -import org.keycloak.freemarker.ThemeLoader; +import org.keycloak.freemarker.ThemeProvider; import org.keycloak.models.AdminRoles; import org.keycloak.models.ApplicationModel; import org.keycloak.models.Constants; @@ -280,7 +281,8 @@ public class AdminConsole { try { //logger.info("getting resource: " + path + " uri: " + uriInfo.getRequestUri().toString()); - Theme theme = ThemeLoader.createTheme(realm.getAdminTheme(), Theme.Type.ADMIN); + ExtendingThemeManager themeManager = new ExtendingThemeManager(providerSession); + Theme theme = themeManager.createTheme(realm.getAdminTheme(), Theme.Type.ADMIN); InputStream resource = theme.getResourceAsStream(path); if (resource != null) { String contentType = mimeTypes.getContentType(path); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 7a089d8067..014fe52f5f 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -133,7 +133,7 @@ public class RealmAdminResource { @Path("users") public UsersResource users() { - UsersResource users = new UsersResource(realm, auth, tokenManager); + UsersResource users = new UsersResource(providers, realm, auth, tokenManager); ResteasyProviderFactory.getInstance().injectProperties(users); //resourceContext.initResource(users); return users; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index feba6f9859..e4cc8c1ab8 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -4,6 +4,8 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.email.EmailException; +import org.keycloak.email.EmailProvider; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -15,6 +17,7 @@ import org.keycloak.models.SocialLinkModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.provider.ProviderSession; import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.idm.ApplicationMappingsRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -23,8 +26,6 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.SocialLinkRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; -import org.keycloak.services.email.EmailException; -import org.keycloak.services.email.EmailSender; import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.RealmManager; @@ -46,6 +47,7 @@ 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.UriBuilder; import javax.ws.rs.core.UriInfo; import java.util.ArrayList; import java.util.HashMap; @@ -53,6 +55,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; /** * @author Bill Burke @@ -63,10 +66,12 @@ public class UsersResource { protected RealmModel realm; + private ProviderSession providerSession; private RealmAuth auth; private TokenManager tokenManager; - public UsersResource(RealmModel realm, RealmAuth auth, TokenManager tokenManager) { + public UsersResource(ProviderSession providerSession, RealmModel realm, RealmAuth auth, TokenManager tokenManager) { + this.providerSession = providerSession; this.auth = auth; this.realm = realm; this.tokenManager = tokenManager; @@ -660,7 +665,7 @@ public class UsersResource { ClientModel client = realm.findClient(clientId); if (client == null || !client.isEnabled()) { - return Flows.errors().error("Account management not enabled", Response.Status.INTERNAL_SERVER_ERROR); + return Flows.errors().error("AccountProvider management not enabled", Response.Status.INTERNAL_SERVER_ERROR); } Set requiredActions = new HashSet(user.getRequiredActions()); @@ -671,7 +676,14 @@ public class UsersResource { accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); try { - new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo); + UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); + builder.queryParam("key", accessCode.getId()); + + String link = builder.build(realm.getName()).toString(); + long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); + + providerSession.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration); + return Response.ok().build(); } catch (EmailException e) { logger.error("Failed to send password reset email", e); 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 2d3aa673a0..388ff82167 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 @@ -22,9 +22,9 @@ package org.keycloak.services.resources.flows; import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.login.LoginForms; -import org.keycloak.login.LoginFormsLoader; +import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.RealmModel; +import org.keycloak.provider.ProviderSession; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.TokenManager; @@ -40,13 +40,13 @@ public class Flows { private Flows() { } - public static LoginForms forms(RealmModel realm, UriInfo uriInfo) { - return LoginFormsLoader.load().createForms(realm, uriInfo); + public static LoginFormsProvider forms(ProviderSession session, RealmModel realm, UriInfo uriInfo) { + return session.getProvider(LoginFormsProvider.class).setRealm(realm).setUriInfo(uriInfo); } - public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager, + public static OAuthFlows oauth(ProviderSession session, RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager, TokenManager tokenManager) { - return new OAuthFlows(realm, request, uriInfo, authManager, tokenManager); + return new OAuthFlows(session, realm, request, uriInfo, authManager, tokenManager); } public static SocialRedirectFlows social(SocialRequestManager socialRequestManager, RealmModel realm, UriInfo uriInfo, SocialProvider provider) { diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index d31aab53be..2ed76b0139 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -35,6 +35,7 @@ import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserSessionModel; +import org.keycloak.provider.ProviderSession; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AuthenticationManager; @@ -56,6 +57,8 @@ public class OAuthFlows { private static final Logger log = Logger.getLogger(OAuthFlows.class); + private final ProviderSession providerSession; + private final RealmModel realm; private final HttpRequest request; @@ -66,8 +69,9 @@ public class OAuthFlows { private final TokenManager tokenManager; - OAuthFlows(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager, + OAuthFlows(ProviderSession providerSession, RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager, TokenManager tokenManager) { + this.providerSession = providerSession; this.realm = realm; this.request = request; this.uriInfo = uriInfo; @@ -84,7 +88,7 @@ public class OAuthFlows { String code = accessCode.getCode(); if (Constants.INSTALLED_APP_URN.equals(redirect)) { - return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), code).createCode(); + return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), code).createCode(); } else { UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code); log.debugv("redirectAccessCode: state: {0}", state); @@ -102,7 +106,7 @@ public class OAuthFlows { public Response redirectError(ClientModel client, String error, String state, String redirect) { if (Constants.INSTALLED_APP_URN.equals(redirect)) { - return Flows.forms(realm, uriInfo).setError(error).createCode(); + return Flows.forms(providerSession, realm, uriInfo).setError(error).createCode(); } else { UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, error); if (state != null) { @@ -139,14 +143,14 @@ public class OAuthFlows { audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); } - return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user) + return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()).setUser(user) .createResponse(action); } if (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) { accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); - return Flows.forms(realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()). + return Flows.forms(providerSession, realm, uriInfo).setAccessCode(accessCode.getId(), accessCode.getCode()). setAccessRequest(accessCode.getRealmRolesRequested(), accessCode.getResourceRolesRequested()). setClient(client).createOAuthGrant(); } @@ -160,7 +164,7 @@ public class OAuthFlows { } public Response forwardToSecurityFailure(String message) { - return Flows.forms(realm, uriInfo).setError(message).createErrorPage(); + return Flows.forms(providerSession, realm, uriInfo).setError(message).createErrorPage(); } private void isTotpConfigurationRequired(UserModel user) { diff --git a/services/src/test/java/org/keycloak/services/email/EmailSenderTest.java b/services/src/test/java/org/keycloak/services/email/EmailSenderTest.java deleted file mode 100755 index d08b5cf664..0000000000 --- a/services/src/test/java/org/keycloak/services/email/EmailSenderTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.keycloak.services.email; - -import com.icegreen.greenmail.util.GreenMail; -import com.icegreen.greenmail.util.ServerSetup; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.util.JsonSerialization; - -import javax.mail.MessagingException; -import javax.mail.internet.AddressException; -import javax.mail.internet.MimeMessage; -import java.io.IOException; -import java.lang.Thread.UncaughtExceptionHandler; -import java.net.SocketException; -import java.util.HashMap; -import java.util.UUID; - -public class EmailSenderTest { - - private GreenMail greenMail; - private EmailSender emailSender; - - @Test - public void testUUID() throws Exception{ - System.out.println(UUID.randomUUID()); - - HashMap config = new HashMap(); - config.put("from", "auto@keycloak.org"); - config.put("host", "localhost"); - config.put("port", "3025"); - - System.out.println(JsonSerialization.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(config)); - - - } - - @Before - public void before() { - ServerSetup setup = new ServerSetup(3025, "localhost", "smtp"); - - greenMail = new GreenMail(setup); - greenMail.start(); - - HashMap config = new HashMap(); - config.put("from", "auto@keycloak.org"); - config.put("host", "localhost"); - config.put("port", "3025"); - - emailSender = new EmailSender(config); - } - - @After - public void after() throws InterruptedException { - if (greenMail != null) { - // Suppress error from GreenMail on shutdown - Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - if (!(e.getCause() instanceof SocketException && t.getClass().getName() - .equals("com.icegreen.greenmail.smtp.SmtpHandler"))) { - System.err.print("Exception in thread \"" + t.getName() + "\" "); - e.printStackTrace(System.err); - } - } - }); - - greenMail.stop(); - } - } - - @Test - public void sendMail() throws Exception { - emailSender.send("test@test.com", "Test subject", "Test body"); - - MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); - Assert.assertEquals(1, receivedMessages.length); - - MimeMessage msg = receivedMessages[0]; - Assert.assertEquals(1, msg.getFrom().length); - Assert.assertEquals("auto@keycloak.org", msg.getFrom()[0].toString()); - Assert.assertEquals("Test subject", msg.getSubject()); - Assert.assertEquals("Test body", ((String) msg.getContent()).trim()); - } - -} diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index f54b269296..7924ae959e 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -196,6 +196,16 @@ keycloak-forms-common-themes ${project.version} + + org.keycloak + keycloak-email-api + ${project.version} + + + org.keycloak + keycloak-email-freemarker + ${project.version} + org.keycloak keycloak-account-api diff --git a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json index ee6c8fcae5..6b4cd35adb 100644 --- a/testsuite/integration/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/main/resources/META-INF/keycloak-server.json @@ -20,6 +20,18 @@ "dir": "${keycloak.theme.dir}" }, + "login-forms": { + "provider": "freemarker" + }, + + "account": { + "provider": "freemarker" + }, + + "email": { + "provider": "freemarker" + }, + "scheduled": { "interval": 900 } 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 974ae881e4..9746666333 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 @@ -33,6 +33,7 @@ import org.keycloak.models.UserModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.org.keycloak.testsuite.util.MailUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -113,12 +114,7 @@ public class RequiredActionEmailVerificationTest { MimeMessage message = greenMail.getReceivedMessages()[0]; String body = (String) message.getContent(); - - Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*"); - Matcher m = p.matcher(body); - m.matches(); - - String verificationUrl = m.group(1); + String verificationUrl = MailUtil.getLink(body); Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent(); String sessionId = sendEvent.getSessionId(); @@ -152,16 +148,12 @@ public class RequiredActionEmailVerificationTest { String body = (String) message.getContent(); - Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*"); - Matcher m = p.matcher(body); - m.matches(); - Event sendEvent = events.expectRequiredAction("send_verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent(); String sessionId = sendEvent.getSessionId(); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); - String verificationUrl = m.group(1); + String verificationUrl = MailUtil.getLink(body); driver.navigate().to(verificationUrl.trim()); @@ -194,13 +186,9 @@ public class RequiredActionEmailVerificationTest { String body = (String) message.getContent(); - Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*"); - Matcher m = p.matcher(body); - m.matches(); - events.expectRequiredAction("send_verify_email").session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent); - String verificationUrl = m.group(1); + String verificationUrl = MailUtil.getLink(body); driver.navigate().to(verificationUrl.trim()); 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 e2259f9599..c1066bb89c 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 @@ -35,6 +35,7 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.org.keycloak.testsuite.util.MailUtil; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -133,7 +134,7 @@ public class ResetPasswordTest { MimeMessage message = greenMail.getReceivedMessages()[0]; String body = (String) message.getContent(); - String changePasswordUrl = body.split("\n")[3]; + String changePasswordUrl = MailUtil.getLink(body); driver.navigate().to(changePasswordUrl.trim()); @@ -205,7 +206,7 @@ public class ResetPasswordTest { MimeMessage message = greenMail.getReceivedMessages()[0]; String body = (String) message.getContent(); - String changePasswordUrl = body.split("\n")[3]; + String changePasswordUrl = MailUtil.getLink(body); String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java new file mode 100644 index 0000000000..440fdb823e --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/org/keycloak/testsuite/util/MailUtil.java @@ -0,0 +1,21 @@ +package org.keycloak.testsuite.org.keycloak.testsuite.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Stian Thorgersen + */ +public class MailUtil { + + private static Pattern mailPattern = Pattern.compile("http[^\\s]*"); + + public static String getLink(String body) { + Matcher matcher = mailPattern.matcher(body); + if (matcher.find()) { + return matcher.group(); + } + throw new AssertionError("No link found in " + body); + } + +}