keycloak-uncached
Changes
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccount.java 201(+0 -201)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java 202(+199 -3)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProviderFactory.java 33(+33 -0)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/MessageBean.java 12(+6 -6)
forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProvider 1(+0 -1)
forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory 1(+1 -0)
forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java 70(+56 -14)
forms/common-themes/src/main/java/org/keycloak/theme/DefaultKeycloakThemeProviderFactory.java 35(+35 -0)
forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProvider 2(+0 -2)
forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory 2(+2 -0)
forms/email-api/pom.xml 44(+44 -0)
forms/email-freemarker/pom.xml 71(+71 -0)
forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java 140(+140 -0)
forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProviderFactory.java 31(+31 -0)
forms/email-freemarker/src/main/resources/META-INF/services/org.keycloak.email.EmailProviderFactory 1(+1 -0)
forms/login-freemarker/pom.xml 6(+6 -0)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginForms.java 287(+0 -287)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java 333(+314 -19)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProviderFactory.java 31(+31 -0)
forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProvider 1(+0 -1)
forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory 1(+1 -0)
forms/pom.xml 2(+2 -0)
pom.xml 5(+5 -0)
project-integrations/aerogear-ups/auth-server/src/main/java/org/aerogear/ups/security/AerogearThemeProvider.java 4(+4 -0)
server/pom.xml 10(+10 -0)
services/pom.xml 6(+6 -0)
testsuite/integration/pom.xml 10(+10 -0)
Details
diff --git a/docbook/reference/en/en-US/modules/themes.xml b/docbook/reference/en/en-US/modules/themes.xml
index 5bb39cb..5e4ff8a 100755
--- a/docbook/reference/en/en-US/modules/themes.xml
+++ b/docbook/reference/en/en-US/modules/themes.xml
@@ -148,26 +148,26 @@
<title>Account SPI</title>
<para>
The Account SPI allows implementing the account management pages using whatever web framework or templating
- engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProvider</literal>
- and <literal>org.keycloak.account.Account</literal> in <literal>forms/account-api</literal>.
+ engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProviderFactory</literal>
+ and <literal>org.keycloak.account.AccountProvider</literal> in <literal>forms/account-api</literal>.
</para>
<para>
Keycloaks default account management provider is built on the FreeMarker template engine (<literal>forms/account-freemarker</literal>).
To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-account-freemarker-1.0-beta-1-SNAPSHOT.jar</literal>
- or disable it with the system property <literal>org.keycloak.account.freemarker.FreeMarkerAccountProvider</literal>.
+ or disable it with the system property <literal>org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory</literal>.
</para>
</section>
<section>
<title>Login SPI</title>
<para>
The Login SPI allows implementing the login forms using whatever web framework or templating
- engine you want. To create a Login forms provider implement <literal>org.keycloak.login.LoginFormsProvider</literal>
- and <literal>org.keycloak.login.LoginForms</literal> in <literal>forms/login-api</literal>.
+ engine you want. To create a Login forms provider implement <literal>org.keycloak.login.LoginFormsProviderFactory</literal>
+ and <literal>org.keycloak.login.LoginFormsProvider</literal> in <literal>forms/login-api</literal>.
</para>
<para>
Keycloaks default login forms provider is built on the FreeMarker template engine (<literal>forms/login-freemarker</literal>).
To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-login-freemarker-1.0-beta-1-SNAPSHOT.jar</literal>
- or disable it with the system property <literal>org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider</literal>.
+ or disable it with the system property <literal>org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory</literal>.
</para>
</section>
</section>
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 37ffe89..f021e84 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-public interface AccountProvider {
+public interface AccountProvider extends Provider {
+
+ 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<Event> events);
- public Account createAccount(UriInfo uriInfo);
+ AccountProvider setSessions(List<UserSessionModel> sessions);
+ AccountProvider setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported);
}
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 0000000..e956b14
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..8ab9e17
--- /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/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
old mode 100644
new mode 100755
index 17d8fb9..0400f75
--- 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<Event> events;
+ private List<UserSessionModel> 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 Response createResponse(AccountPages page) {
+ Map<String, Object> attributes = new HashMap<String, Object>();
+
+ 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<String, List<String>> 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<Event> events) {
+ this.events = events;
+ return this;
+ }
+
+ @Override
+ public AccountProvider setSessions(List<UserSessionModel> 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 Account createAccount(UriInfo uriInfo) {
- return new FreeMarkerAccount(uriInfo);
+ 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 0000000..7a3f272
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 08f156a..6fc48be 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -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.AccountProviderFactory b/forms/account-freemarker/src/main/resources/META-INF/services/org.keycloak.account.AccountProviderFactory
new file mode 100644
index 0000000..fd99df6
--- /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/Theme.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/Theme.java
index dd02e62..14794fb 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 6ca677c..07ee09a 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
-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 0000000..26ce238
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface ThemeProviderFactory extends ProviderFactory<ThemeProvider> {
+}
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 0000000..c3d738b
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..4ba538b
--- /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 8a8659c..47f1fdd 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<String> ACCOUNT_THEMES = new HashSet<String>();
private static Set<String> LOGIN_THEMES = new HashSet<String>();
private static Set<String> ADMIN_THEMES = new HashSet<String>();
+ private static Set<String> EMAIL_THEMES = new HashSet<String>();
private static Set<String> COMMON_THEMES = new HashSet<String>();
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 0000000..4c3515d
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 101851c..96cac2f 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 0000000..ff9dd2e
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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.ThemeProviderFactory b/forms/common-themes/src/main/resources/META-INF/services/org.keycloak.freemarker.ThemeProviderFactory
new file mode 100644
index 0000000..c1b32dc
--- /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 0000000..38d150f
--- /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 0000000..3139aca
--- /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 0000000..5d277e5
--- /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
forms/email-api/pom.xml 44(+44 -0)
diff --git a/forms/email-api/pom.xml b/forms/email-api/pom.xml
new file mode 100755
index 0000000..19ef08a
--- /dev/null
+++ b/forms/email-api/pom.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-forms</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-beta-1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-email-api</artifactId>
+ <name>Keycloak Email API</name>
+ <description />
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-model-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
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 0000000..0827b96
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..02d7daf
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public interface EmailProviderFactory extends ProviderFactory<EmailProvider> {
+}
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 0000000..cb2877b
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..4110dba
--- /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
forms/email-freemarker/pom.xml 71(+71 -0)
diff --git a/forms/email-freemarker/pom.xml b/forms/email-freemarker/pom.xml
new file mode 100755
index 0000000..b5fb4de
--- /dev/null
+++ b/forms/email-freemarker/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-forms</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.0-beta-1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-email-freemarker</artifactId>
+ <name>Keycloak Email FreeMarker</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-email-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-model-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-forms-common-freemarker</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.freemarker</groupId>
+ <artifactId>freemarker</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>mail</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
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 0000000..9590a9c
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<String, Object> attributes = new HashMap<String, Object>();
+ 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<String, Object> attributes = new HashMap<String, Object>();
+ attributes.put("link", link);
+ attributes.put("linkExpiration", expirationInMinutes);
+
+ send("emailVerificationSubject", "email-verification.ftl", attributes);
+ }
+
+ private void send(String subjectKey, String template, Map<String, Object> 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<String, String> 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 0000000..b98d981
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..5b31f6e
--- /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/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
index be59cde..c686cf9 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
- */
-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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> 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<String, String> queryParams);
+
+ public LoginFormsProvider setFormData(MultivaluedMap<String, String> formData);
+
+ public LoginFormsProvider setStatus(Response.Status status);
+
+}
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 0000000..e3fffea
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 0000000..afbdfbf
--- /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
forms/login-freemarker/pom.xml 6(+6 -0)
diff --git a/forms/login-freemarker/pom.xml b/forms/login-freemarker/pom.xml
index c9e85bf..9e51232 100755
--- a/forms/login-freemarker/pom.xml
+++ b/forms/login-freemarker/pom.xml
@@ -32,6 +32,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-email-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
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 5f0b0c5..1dfcd29 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
- */
-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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<RoleModel> realmRolesRequested;
+ private MultivaluedMap<String, RoleModel> resourceRolesRequested;
+ private MultivaluedMap<String, String> queryParams;
+
+ public static enum MessageType {SUCCESS, WARNING, ERROR}
+
+ private MessageType messageType = MessageType.ERROR;
+
+ private MultivaluedMap<String, String> 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<String, String> 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<String, Object> attributes = new HashMap<String, Object>();
+
+ 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<String, String> 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<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> 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<String, String> 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 0000000..6b11d19
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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 a6b36d4..72d48b7 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -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.LoginFormsProviderFactory b/forms/login-freemarker/src/main/resources/META-INF/services/org.keycloak.login.LoginFormsProviderFactory
new file mode 100644
index 0000000..893783b
--- /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
forms/pom.xml 2(+2 -0)
diff --git a/forms/pom.xml b/forms/pom.xml
index f70ac61..0f2268d 100755
--- a/forms/pom.xml
+++ b/forms/pom.xml
@@ -19,6 +19,8 @@
<module>common-themes</module>
<module>account-api</module>
<module>account-freemarker</module>
+ <module>email-api</module>
+ <module>email-freemarker</module>
<module>login-api</module>
<module>login-freemarker</module>
</modules>
pom.xml 5(+5 -0)
diff --git a/pom.xml b/pom.xml
index 335383b..0aa3dff 100755
--- a/pom.xml
+++ b/pom.xml
@@ -122,6 +122,11 @@
<version>2.3.8</version>
</dependency>
<dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>mail</artifactId>
+ <version>1.4.7</version>
+ </dependency>
+ <dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jaxrs-api</artifactId>
<version>${resteasy.version}</version>
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 d8ef5e0..650af91 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() {
+ }
+
}
server/pom.xml 10(+10 -0)
diff --git a/server/pom.xml b/server/pom.xml
index 1276b48..e353dd5 100755
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -119,6 +119,16 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-email-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-email-freemarker</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-login-api</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/server/src/main/resources/META-INF/keycloak-server.json b/server/src/main/resources/META-INF/keycloak-server.json
index 62405d9..544ad81 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
}
services/pom.xml 6(+6 -0)
diff --git a/services/pom.xml b/services/pom.xml
index 6b8ceef..7612a20 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -51,6 +51,12 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-email-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-login-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
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 453f550..c23858d 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 85a1461..5f6206f 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/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java
index eaf7666..bd897fa 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 7a089d8..014fe52 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 feba6f9..e4cc8c1 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -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<UserModel.RequiredAction> requiredActions = new HashSet<UserModel.RequiredAction>(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 2d3aa67..388ff82 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 d31aab5..2ed76b0 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/main/java/org/keycloak/services/resources/RequiredActionsService.java b/services/src/main/java/org/keycloak/services/resources/RequiredActionsService.java
index 4c6139c..4c1e062 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -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<RequiredAction> 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 06f0ae6..76c8985 100755
--- a/services/src/main/java/org/keycloak/services/resources/SocialResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/SocialResource.java
@@ -97,6 +97,9 @@ public class SocialResource {
*/
@Context
+ protected ProviderSession providerSession;
+
+ @Context
protected KeycloakSession session;
@Context
@@ -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 cc04a57..1d4fe1c 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 d9c12de..9b47de5 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<String, String>();
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<String, String>();
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<String, String> 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");
testsuite/integration/pom.xml 10(+10 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index f54b269..7924ae9 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -198,6 +198,16 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-email-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-email-freemarker</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-account-api</artifactId>
<version>${project.version}</version>
</dependency>
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 ee6c8fc..6b4cd35 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 974ae88..9746666 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 e2259f9..c1066bb 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 0000000..440fdb8
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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);
+ }
+
+}