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 extends Provider> getProviderClass() {
+ return AccountProvider.class;
+ }
+
+ @Override
+ public Class extends ProviderFactory> 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 extends Provider> getProviderClass() {
+ return ThemeProvider.class;
+ }
+
+ @Override
+ public Class extends ProviderFactory> 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.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 extends Provider> getProviderClass() {
+ return EmailProvider.class;
+ }
+
+ @Override
+ public Class extends ProviderFactory> 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.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 extends Provider> getProviderClass() {
+ return LoginFormsProvider.class;
+ }
+
+ @Override
+ public Class extends ProviderFactory> 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);
+ }
+
+}