Added theme support to emails

This commit is contained in:
Stian Thorgersen 2014-05-19 12:58:45 +01:00
parent 7e8b16f975
commit a3d08e7191
75 changed files with 1513 additions and 1041 deletions

View file

@ -148,26 +148,26 @@
<title>Account SPI</title> <title>Account SPI</title>
<para> <para>
The Account SPI allows implementing the account management pages using whatever web framework or templating 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> engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProviderFactory</literal>
and <literal>org.keycloak.account.Account</literal> in <literal>forms/account-api</literal>. and <literal>org.keycloak.account.AccountProvider</literal> in <literal>forms/account-api</literal>.
</para> </para>
<para> <para>
Keycloaks default account management provider is built on the FreeMarker template engine (<literal>forms/account-freemarker</literal>). 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> 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> </para>
</section> </section>
<section> <section>
<title>Login SPI</title> <title>Login SPI</title>
<para> <para>
The Login SPI allows implementing the login forms using whatever web framework or templating 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> engine you want. To create a Login forms provider implement <literal>org.keycloak.login.LoginFormsProviderFactory</literal>
and <literal>org.keycloak.login.LoginForms</literal> in <literal>forms/login-api</literal>. and <literal>org.keycloak.login.LoginFormsProvider</literal> in <literal>forms/login-api</literal>.
</para> </para>
<para> <para>
Keycloaks default login forms provider is built on the FreeMarker template engine (<literal>forms/login-freemarker</literal>). 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> 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> </para>
</section> </section>
</section> </section>

View file

@ -1,38 +0,0 @@
package org.keycloak.account;
import org.keycloak.audit.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface Account {
Response createResponse(AccountPages page);
Account setError(String message);
Account setSuccess(String message);
Account setWarning(String message);
Account setUser(UserModel user);
Account setStatus(Response.Status status);
Account setRealm(RealmModel realm);
Account setReferrer(String[] referrer);
Account setEvents(List<Event> events);
Account setSessions(List<UserSessionModel> sessions);
Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported);
}

View file

@ -1,17 +0,0 @@
package org.keycloak.account;
import java.util.ServiceLoader;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountLoader {
private AccountLoader() {
}
public static AccountProvider load() {
return ServiceLoader.load(AccountProvider.class).iterator().next();
}
}

View file

@ -1,12 +1,41 @@
package org.keycloak.account; 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 javax.ws.rs.core.UriInfo;
import java.util.List;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public interface AccountProvider { public interface AccountProvider extends Provider {
public Account createAccount(UriInfo uriInfo); AccountProvider setUriInfo(UriInfo uriInfo);
Response createResponse(AccountPages page);
AccountProvider setError(String message);
AccountProvider setSuccess(String message);
AccountProvider setWarning(String message);
AccountProvider setUser(UserModel user);
AccountProvider setStatus(Response.Status status);
AccountProvider setRealm(RealmModel realm);
AccountProvider setReferrer(String[] referrer);
AccountProvider setEvents(List<Event> events);
AccountProvider setSessions(List<UserSessionModel> sessions);
AccountProvider setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported);
} }

View file

@ -0,0 +1,10 @@
package org.keycloak.account;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface AccountProviderFactory extends ProviderFactory {
}

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
org.keycloak.account.AccountSpi

View file

@ -1,201 +0,0 @@
package org.keycloak.account.freemarker;
import org.jboss.logging.Logger;
import org.keycloak.account.Account;
import org.keycloak.account.AccountPages;
import org.keycloak.account.freemarker.model.AccountBean;
import org.keycloak.account.freemarker.model.AccountSocialBean;
import org.keycloak.account.freemarker.model.FeaturesBean;
import org.keycloak.account.freemarker.model.LogBean;
import org.keycloak.account.freemarker.model.MessageBean;
import org.keycloak.account.freemarker.model.ReferrerBean;
import org.keycloak.account.freemarker.model.SessionsBean;
import org.keycloak.account.freemarker.model.TotpBean;
import org.keycloak.account.freemarker.model.UrlBean;
import org.keycloak.audit.Event;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeLoader;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerAccount implements Account {
private static final Logger logger = Logger.getLogger(FreeMarkerAccount.class);
private UserModel user;
private Response.Status status = Response.Status.OK;
private RealmModel realm;
private String[] referrer;
private List<Event> events;
private List<UserSessionModel> sessions;
private boolean social;
private boolean audit;
private boolean passwordUpdateSupported;
public static enum MessageType {SUCCESS, WARNING, ERROR}
private UriInfo uriInfo;
private String message;
private MessageType messageType;
public FreeMarkerAccount(UriInfo uriInfo) {
this.uriInfo = uriInfo;
}
@Override
public Response createResponse(AccountPages page) {
Map<String, Object> attributes = new HashMap<String, Object>();
Theme theme;
try {
theme = ThemeLoader.createTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT);
} catch (FreeMarkerException e) {
logger.error("Failed to create theme", e);
return Response.serverError().build();
}
try {
attributes.put("properties", theme.getProperties());
} catch (IOException e) {
logger.warn("Failed to load properties", e);
}
Properties messages;
try {
messages = theme.getMessages();
attributes.put("rb", messages);
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messages = new Properties();
}
URI baseUri = uriInfo.getBaseUri();
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
for (Map.Entry<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 Account setError(String message) {
this.message = message;
this.messageType = MessageType.ERROR;
return this;
}
@Override
public Account setSuccess(String message) {
this.message = message;
this.messageType = MessageType.SUCCESS;
return this;
}
@Override
public Account setWarning(String message) {
this.message = message;
this.messageType = MessageType.WARNING;
return this;
}
@Override
public Account setUser(UserModel user) {
this.user = user;
return this;
}
@Override
public Account setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
@Override
public Account setStatus(Response.Status status) {
this.status = status;
return this;
}
@Override
public Account setReferrer(String[] referrer) {
this.referrer = referrer;
return this;
}
@Override
public Account setEvents(List<Event> events) {
this.events = events;
return this;
}
@Override
public Account setSessions(List<UserSessionModel> sessions) {
this.sessions = sessions;
return this;
}
@Override
public Account setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) {
this.social = social;
this.audit = audit;
this.passwordUpdateSupported = passwordUpdateSupported;
return this;
}
}

View file

@ -1,18 +1,214 @@
package org.keycloak.account.freemarker; 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.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 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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class FreeMarkerAccountProvider implements AccountProvider { 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 @Override
public Account createAccount(UriInfo uriInfo) { public Response createResponse(AccountPages page) {
return new FreeMarkerAccount(uriInfo); 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 void close() {
} }
} }

View file

@ -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";
}
}

View file

@ -21,7 +21,7 @@
*/ */
package org.keycloak.account.freemarker.model; 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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -30,9 +30,9 @@ public class MessageBean {
private String summary; 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.summary = message;
this.type = type; this.type = type;
} }
@ -46,15 +46,15 @@ public class MessageBean {
} }
public boolean isSuccess() { public boolean isSuccess() {
return FreeMarkerAccount.MessageType.SUCCESS.equals(this.type); return FreeMarkerAccountProvider.MessageType.SUCCESS.equals(this.type);
} }
public boolean isWarning() { public boolean isWarning() {
return FreeMarkerAccount.MessageType.WARNING.equals(this.type); return FreeMarkerAccountProvider.MessageType.WARNING.equals(this.type);
} }
public boolean isError() { public boolean isError() {
return FreeMarkerAccount.MessageType.ERROR.equals(this.type); return FreeMarkerAccountProvider.MessageType.ERROR.equals(this.type);
} }
} }

View file

@ -1 +0,0 @@
org.keycloak.account.freemarker.FreeMarkerAccountProvider

View file

@ -0,0 +1 @@
org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory

View file

@ -1,31 +1,35 @@
package org.keycloak.freemarker; package org.keycloak.freemarker;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.util.ProviderLoader; import org.keycloak.provider.ProviderSession;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Properties; import java.util.Properties;
import java.util.Set;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class ThemeLoader { public class ExtendingThemeManager implements ThemeProvider {
public static Theme createTheme(String name, Theme.Type type) throws FreeMarkerException { private List<ThemeProvider> providers;
if (name == null) { private String defaultTheme;
name = Config.scope("theme").get("default");
}
List<ThemeProvider> providers = new LinkedList(); public ExtendingThemeManager(ProviderSession providerSession) {
for (ThemeProvider p : ProviderLoader.load(ThemeProvider.class)) { providers = new LinkedList();
providers.add(p);
for (ThemeProvider p : providerSession.getAllProviders(ThemeProvider.class)) {
if (!p.getClass().equals(ExtendingThemeManager.class)) {
providers.add(p);
}
} }
Collections.sort(providers, new Comparator<ThemeProvider>() { Collections.sort(providers, new Comparator<ThemeProvider>() {
@ -35,23 +39,37 @@ public class ThemeLoader {
} }
}); });
Theme theme = findTheme(providers, name, type); this.defaultTheme = Config.scope("theme").get("default");
}
@Override
public int getProviderPriority() {
return 0;
}
@Override
public Theme createTheme(String name, Theme.Type type) throws IOException {
if (name == null) {
name = defaultTheme;
}
Theme theme = findTheme(name, type);
if (theme.getParentName() != null) { if (theme.getParentName() != null) {
List<Theme> themes = new LinkedList<Theme>(); List<Theme> themes = new LinkedList<Theme>();
themes.add(theme); themes.add(theme);
if (theme.getImportName() != null) { if (theme.getImportName() != null) {
String[] s = theme.getImportName().split("/"); String[] s = theme.getImportName().split("/");
themes.add(findTheme(providers, s[1], Theme.Type.valueOf(s[0].toUpperCase()))); themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase())));
} }
for (String parentName = theme.getParentName(); parentName != null; parentName = theme.getParentName()) { for (String parentName = theme.getParentName(); parentName != null; parentName = theme.getParentName()) {
theme = findTheme(providers, parentName, type); theme = findTheme(parentName, type);
themes.add(theme); themes.add(theme);
if (theme.getImportName() != null) { if (theme.getImportName() != null) {
String[] s = theme.getImportName().split("/"); String[] s = theme.getImportName().split("/");
themes.add(findTheme(providers, s[1], Theme.Type.valueOf(s[0].toUpperCase()))); themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase())));
} }
} }
@ -61,7 +79,31 @@ public class ThemeLoader {
} }
} }
private static Theme findTheme(Iterable<ThemeProvider> providers, String name, Theme.Type type) { @Override
public Set<String> nameSet(Theme.Type type) {
Set<String> themes = new HashSet<String>();
for (ThemeProvider p : providers) {
themes.addAll(p.nameSet(type));
}
return themes;
}
@Override
public boolean hasTheme(String name, Theme.Type type) {
for (ThemeProvider p : providers) {
if (p.hasTheme(name, type)) {
return true;
}
}
return false;
}
@Override
public void close() {
providers = null;
}
private Theme findTheme(String name, Theme.Type type) {
for (ThemeProvider p : providers) { for (ThemeProvider p : providers) {
if (p.hasTheme(name, type)) { if (p.hasTheme(name, type)) {
try { try {

View file

@ -10,7 +10,7 @@ import java.util.Properties;
*/ */
public interface Theme { public interface Theme {
public enum Type { LOGIN, ACCOUNT, ADMIN, COMMON }; public enum Type { LOGIN, ACCOUNT, ADMIN, EMAIL, COMMON };
public String getName(); public String getName();

View file

@ -1,12 +1,14 @@
package org.keycloak.freemarker; package org.keycloak.freemarker;
import org.keycloak.provider.Provider;
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.Set;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public interface ThemeProvider { public interface ThemeProvider extends Provider {
public int getProviderPriority(); public int getProviderPriority();

View file

@ -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> {
}

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
org.keycloak.freemarker.ThemeSpi

View file

@ -20,12 +20,14 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider {
private static Set<String> ACCOUNT_THEMES = new HashSet<String>(); private static Set<String> ACCOUNT_THEMES = new HashSet<String>();
private static Set<String> LOGIN_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> ADMIN_THEMES = new HashSet<String>();
private static Set<String> EMAIL_THEMES = new HashSet<String>();
private static Set<String> COMMON_THEMES = new HashSet<String>(); private static Set<String> COMMON_THEMES = new HashSet<String>();
static { static {
Collections.addAll(ACCOUNT_THEMES, BASE, PATTERNFLY, KEYCLOAK); Collections.addAll(ACCOUNT_THEMES, BASE, PATTERNFLY, KEYCLOAK);
Collections.addAll(LOGIN_THEMES, BASE, PATTERNFLY, KEYCLOAK); Collections.addAll(LOGIN_THEMES, BASE, PATTERNFLY, KEYCLOAK);
Collections.addAll(ADMIN_THEMES, BASE, PATTERNFLY, KEYCLOAK); Collections.addAll(ADMIN_THEMES, BASE, PATTERNFLY, KEYCLOAK);
Collections.addAll(EMAIL_THEMES, KEYCLOAK);
Collections.addAll(COMMON_THEMES, KEYCLOAK); Collections.addAll(COMMON_THEMES, KEYCLOAK);
} }
@ -52,6 +54,8 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider {
return ACCOUNT_THEMES; return ACCOUNT_THEMES;
case ADMIN: case ADMIN:
return ADMIN_THEMES; return ADMIN_THEMES;
case EMAIL:
return EMAIL_THEMES;
case COMMON: case COMMON:
return COMMON_THEMES; return COMMON_THEMES;
default: default:
@ -64,4 +68,8 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider {
return nameSet(type).contains(name); return nameSet(type).contains(name);
} }
@Override
public void close() {
}
} }

View file

@ -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";
}
}

View file

@ -18,11 +18,8 @@ public class FolderThemeProvider implements ThemeProvider {
private File rootDir; private File rootDir;
public FolderThemeProvider() { public FolderThemeProvider(File rootDir) {
String d = Config.scope("theme").get("dir"); this.rootDir = rootDir;
if (d != null) {
rootDir = new File(d);
}
} }
@Override @Override
@ -75,4 +72,8 @@ public class FolderThemeProvider implements ThemeProvider {
return typeDir != null && new File(typeDir, name).isDirectory(); return typeDir != null && new File(typeDir, name).isDirectory();
} }
@Override
public void close() {
}
} }

View file

@ -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";
}
}

View file

@ -1,2 +0,0 @@
org.keycloak.theme.DefaultKeycloakThemeProvider
org.keycloak.theme.FolderThemeProvider

View file

@ -0,0 +1,2 @@
org.keycloak.theme.DefaultKeycloakThemeProviderFactory
org.keycloak.theme.FolderThemeProviderFactory

View file

@ -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.

View file

@ -0,0 +1,2 @@
emailVerificationSubject=Verify email
passwordResetSubject=Reset password

View file

@ -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.

44
forms/email-api/pom.xml Executable file
View file

@ -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>

View file

@ -1,4 +1,4 @@
package org.keycloak.services.email; package org.keycloak.email;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -9,4 +9,7 @@ public class EmailException extends Exception {
super(cause); super(cause);
} }
public EmailException(String message, Throwable cause) {
super(message, cause);
}
} }

View file

@ -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;
}

View file

@ -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> {
}

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
org.keycloak.email.EmailSpi

71
forms/email-freemarker/pom.xml Executable file
View file

@ -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>

View file

@ -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() {
}
}

View file

@ -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";
}
}

View file

@ -0,0 +1 @@
org.keycloak.email.freemarker.FreeMarkerEmailProviderFactory

View file

@ -1,52 +0,0 @@
package org.keycloak.login;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface LoginForms {
public Response createResponse(UserModel.RequiredAction action);
public Response createLogin();
public Response createPasswordReset();
public Response createLoginTotp();
public Response createRegistration();
public Response createErrorPage();
public Response createOAuthGrant();
public Response createCode();
public LoginForms setAccessCode(String accessCodeId, String accessCode);
public LoginForms setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
public LoginForms setError(String message);
public LoginForms setSuccess(String message);
public LoginForms setWarning(String message);
public LoginForms setUser(UserModel user);
public LoginForms setClient(ClientModel client);
public LoginForms setQueryParams(MultivaluedMap<String, String> queryParams);
public LoginForms setFormData(MultivaluedMap<String, String> formData);
public LoginForms setStatus(Response.Status status);
}

View file

@ -1,17 +0,0 @@
package org.keycloak.login;
import java.util.ServiceLoader;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LoginFormsLoader {
private LoginFormsLoader() {
}
public static LoginFormsProvider load() {
return ServiceLoader.load(LoginFormsProvider.class).iterator().next();
}
}

View file

@ -1,14 +1,59 @@
package org.keycloak.login; package org.keycloak.login;
import org.keycloak.models.RealmModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import javax.ws.rs.core.UriInfo; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
/** import org.keycloak.provider.Provider;
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ import javax.ws.rs.core.MultivaluedMap;
public interface LoginFormsProvider { import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
public LoginForms createForms(RealmModel realm, UriInfo 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);
}

View file

@ -0,0 +1,11 @@
package org.keycloak.login;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderSession;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface LoginFormsProviderFactory extends ProviderFactory {
}

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
org.keycloak.login.LoginFormsSpi

View file

@ -32,6 +32,12 @@
<version>${project.version}</version> <version>${project.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-email-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId> <artifactId>keycloak-model-api</artifactId>

View file

@ -1,287 +0,0 @@
package org.keycloak.login.freemarker;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeLoader;
import org.keycloak.login.LoginForms;
import org.keycloak.login.LoginFormsPages;
import org.keycloak.login.freemarker.model.CodeBean;
import org.keycloak.login.freemarker.model.LoginBean;
import org.keycloak.login.freemarker.model.MessageBean;
import org.keycloak.login.freemarker.model.OAuthGrantBean;
import org.keycloak.login.freemarker.model.ProfileBean;
import org.keycloak.login.freemarker.model.RealmBean;
import org.keycloak.login.freemarker.model.RegisterBean;
import org.keycloak.login.freemarker.model.SocialBean;
import org.keycloak.login.freemarker.model.TotpBean;
import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.email.EmailException;
import org.keycloak.services.email.EmailSender;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerLoginForms implements LoginForms {
private static final Logger logger = Logger.getLogger(FreeMarkerLoginForms.class);
private String message;
private String accessCodeId;
private String accessCode;
private Response.Status status = Response.Status.OK;
private List<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 RealmModel realm;
private UserModel user;
private ClientModel client;
private UriInfo uriInfo;
FreeMarkerLoginForms(RealmModel realm, UriInfo uriInfo) {
this.realm = realm;
this.uriInfo = uriInfo;
}
public Response createResponse(UserModel.RequiredAction action) {
String actionMessage;
LoginFormsPages page;
switch (action) {
case CONFIGURE_TOTP:
actionMessage = Messages.ACTION_WARN_TOTP;
page = LoginFormsPages.LOGIN_CONFIG_TOTP;
break;
case UPDATE_PROFILE:
actionMessage = Messages.ACTION_WARN_PROFILE;
page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
break;
case UPDATE_PASSWORD:
actionMessage = Messages.ACTION_WARN_PASSWD;
page = LoginFormsPages.LOGIN_UPDATE_PASSWORD;
break;
case VERIFY_EMAIL:
try {
new EmailSender(realm.getSmtpConfig()).sendEmailVerification(user, realm, accessCodeId, uriInfo);
} catch (EmailException e) {
return setError("emailSendError").createErrorPage();
}
actionMessage = Messages.ACTION_WARN_EMAIL;
page = LoginFormsPages.LOGIN_VERIFY_EMAIL;
break;
default:
return Response.serverError().build();
}
if (message == null) {
setWarning(actionMessage);
}
return createResponse(page);
}
private Response createResponse(LoginFormsPages page) {
MultivaluedMap<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>();
Theme theme;
try {
theme = ThemeLoader.createTheme(realm.getLoginTheme(), Theme.Type.LOGIN);
} catch (FreeMarkerException e) {
logger.error("Failed to create theme", e);
return Response.serverError().build();
}
try {
attributes.put("properties", theme.getProperties());
} catch (IOException e) {
logger.warn("Failed to load properties", e);
}
Properties messages;
try {
messages = theme.getMessages();
attributes.put("rb", messages);
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messages = new Properties();
}
if (message != null) {
attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType));
}
if (page == LoginFormsPages.OAUTH_GRANT) {
// for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
uriBuilder.replaceQuery(null);
}
URI baseUri = uriBuilder.build();
if (realm != null) {
attributes.put("realm", new RealmBean(realm));
attributes.put("social", new SocialBean(realm, baseUri));
attributes.put("url", new UrlBean(realm, theme, baseUri));
}
attributes.put("login", new LoginBean(formData));
switch (page) {
case LOGIN_CONFIG_TOTP:
attributes.put("totp", new TotpBean(realm, user, baseUri));
break;
case LOGIN_UPDATE_PROFILE:
attributes.put("user", new ProfileBean(user));
break;
case REGISTER:
attributes.put("register", new RegisterBean(formData));
break;
case OAUTH_GRANT:
attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested));
break;
case CODE:
attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null));
break;
}
try {
String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme);
return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
return Response.serverError().build();
}
}
public Response createLogin() {
return createResponse(LoginFormsPages.LOGIN);
}
public Response createPasswordReset() {
return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD);
}
public Response createLoginTotp() {
return createResponse(LoginFormsPages.LOGIN_TOTP);
}
public Response createRegistration() {
return createResponse(LoginFormsPages.REGISTER);
}
public Response createErrorPage() {
setStatus(Response.Status.INTERNAL_SERVER_ERROR);
return createResponse(LoginFormsPages.ERROR);
}
public Response createOAuthGrant() {
return createResponse(LoginFormsPages.OAUTH_GRANT);
}
@Override
public Response createCode() {
return createResponse(LoginFormsPages.CODE);
}
public FreeMarkerLoginForms setError(String message) {
this.message = message;
this.messageType = MessageType.ERROR;
return this;
}
public FreeMarkerLoginForms setSuccess(String message) {
this.message = message;
this.messageType = MessageType.SUCCESS;
return this;
}
public FreeMarkerLoginForms setWarning(String message) {
this.message = message;
this.messageType = MessageType.WARNING;
return this;
}
public FreeMarkerLoginForms setUser(UserModel user) {
this.user = user;
return this;
}
public FreeMarkerLoginForms setClient(ClientModel client) {
this.client = client;
return this;
}
public FreeMarkerLoginForms setFormData(MultivaluedMap<String, String> formData) {
this.formData = formData;
return this;
}
@Override
public LoginForms setAccessCode(String accessCodeId, String accessCode) {
this.accessCodeId = accessCodeId;
this.accessCode = accessCode;
return this;
}
@Override
public LoginForms setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
this.realmRolesRequested = realmRolesRequested;
this.resourceRolesRequested = resourceRolesRequested;
return this;
}
@Override
public LoginForms setStatus(Response.Status status) {
this.status = status;
return this;
}
@Override
public LoginForms setQueryParams(MultivaluedMap<String, String> queryParams) {
this.queryParams = queryParams;
return this;
}
}

View file

@ -1,19 +1,314 @@
package org.keycloak.login.freemarker; package org.keycloak.login.freemarker;
import org.keycloak.login.LoginForms; import org.jboss.logging.Logger;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.OAuth2Constants;
import org.keycloak.models.RealmModel; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import javax.ws.rs.core.UriInfo; import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.FreeMarkerException;
/** import org.keycloak.freemarker.FreeMarkerUtil;
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> import org.keycloak.freemarker.Theme;
*/ import org.keycloak.freemarker.ThemeProvider;
public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { import org.keycloak.login.LoginFormsProvider;
import org.keycloak.login.LoginFormsPages;
@Override import org.keycloak.login.freemarker.model.CodeBean;
public LoginForms createForms(RealmModel realm, UriInfo uriInfo) { import org.keycloak.login.freemarker.model.LoginBean;
return new FreeMarkerLoginForms(realm, uriInfo); 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() {
}
}

View file

@ -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";
}
}

View file

@ -21,7 +21,7 @@
*/ */
package org.keycloak.login.freemarker.model; 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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -30,9 +30,9 @@ public class MessageBean {
private String summary; 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.summary = message;
this.type = type; this.type = type;
} }
@ -46,15 +46,15 @@ public class MessageBean {
} }
public boolean isSuccess() { public boolean isSuccess() {
return FreeMarkerLoginForms.MessageType.SUCCESS.equals(this.type); return FreeMarkerLoginFormsProvider.MessageType.SUCCESS.equals(this.type);
} }
public boolean isWarning() { public boolean isWarning() {
return FreeMarkerLoginForms.MessageType.WARNING.equals(this.type); return FreeMarkerLoginFormsProvider.MessageType.WARNING.equals(this.type);
} }
public boolean isError() { public boolean isError() {
return FreeMarkerLoginForms.MessageType.ERROR.equals(this.type); return FreeMarkerLoginFormsProvider.MessageType.ERROR.equals(this.type);
} }
} }

View file

@ -1 +0,0 @@
org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider

View file

@ -0,0 +1 @@
org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory

View file

@ -19,6 +19,8 @@
<module>common-themes</module> <module>common-themes</module>
<module>account-api</module> <module>account-api</module>
<module>account-freemarker</module> <module>account-freemarker</module>
<module>email-api</module>
<module>email-freemarker</module>
<module>login-api</module> <module>login-api</module>
<module>login-freemarker</module> <module>login-freemarker</module>
</modules> </modules>

View file

@ -121,6 +121,11 @@
<artifactId>base64</artifactId> <artifactId>base64</artifactId>
<version>2.3.8</version> <version>2.3.8</version>
</dependency> </dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<dependency> <dependency>
<groupId>org.jboss.resteasy</groupId> <groupId>org.jboss.resteasy</groupId>
<artifactId>jaxrs-api</artifactId> <artifactId>jaxrs-api</artifactId>

View file

@ -59,4 +59,8 @@ public class AerogearThemeProvider implements ThemeProvider {
return nameSet(type).contains(name); return nameSet(type).contains(name);
} }
@Override
public void close() {
}
} }

View file

@ -117,6 +117,16 @@
<artifactId>keycloak-account-freemarker</artifactId> <artifactId>keycloak-account-freemarker</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </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> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-login-api</artifactId> <artifactId>keycloak-login-api</artifactId>

View file

@ -20,6 +20,18 @@
"dir": "${jboss.server.config.dir}/themes" "dir": "${jboss.server.config.dir}/themes"
}, },
"login-forms": {
"provider": "freemarker"
},
"account": {
"provider": "freemarker"
},
"email": {
"provider": "freemarker"
},
"scheduled": { "scheduled": {
"interval": 900 "interval": 900
} }

View file

@ -49,6 +49,12 @@
<version>${project.version}</version> <version>${project.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-email-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-login-api</artifactId> <artifactId>keycloak-login-api</artifactId>

View file

@ -1,162 +0,0 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.services.email;
import org.jboss.logging.Logger;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.resources.flows.Urls;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class EmailSender {
private static final Logger log = Logger.getLogger(EmailSender.class);
private Map<String, String> config;
public EmailSender(Map<String, String> config) {
this.config = config;
}
public void send(String address, String subject, String body) throws EmailException {
try {
Properties props = new Properties();
props.setProperty("mail.smtp.host", config.get("host"));
boolean auth = "true".equals(config.get("auth"));
boolean ssl = "true".equals(config.get("ssl"));
boolean starttls = "true".equals(config.get("starttls"));
if (config.containsKey("port")) {
props.setProperty("mail.smtp.port", config.get("port"));
}
if (auth) {
props.put("mail.smtp.auth", "true");
}
if (ssl) {
props.put("mail.smtp.socketFactory.port", config.get("port"));
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
}
if (starttls) {
props.put("mail.smtp.starttls.enable", "true");
}
String from = config.get("from");
Session session = Session.getInstance(props);
Message msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(from));
msg.setHeader("To", address);
msg.setSubject(subject);
msg.setText(body);
msg.saveChanges();
Transport transport = session.getTransport("smtp");
if (auth) {
transport.connect(config.get("user"), config.get("password"));
} else {
transport.connect();
}
transport.sendMessage(msg, new InternetAddress[]{new InternetAddress(address)});
} catch (Exception e) {
log.warn("Failed to send email", e);
throw new EmailException(e);
}
}
public void sendEmailVerification(UserModel user, RealmModel realm, String accessCodeId, UriInfo uriInfo) throws EmailException {
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
builder.queryParam("key", accessCodeId);
URI uri = builder.build(realm.getName());
StringBuilder sb = getHeader(user);
sb.append("Someone has created a Keycloak account with this email address. ");
sb.append("If this was you, click the link below to verify your email address:\n");
sb.append(uri.toString());
sb.append("\n\nThis link will expire within ").append(TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
sb.append(" minutes.\n\n");
sb.append("If you didn't create this account, just ignore this message.\n");
addFooter(sb);
send(user.getEmail(), "Verify email", sb.toString());
}
public void sendPasswordReset(UserModel user, RealmModel realm, AccessCodeEntry accessCode, UriInfo uriInfo) throws EmailException {
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
builder.queryParam("key", accessCode.getId());
URI uri = builder.build(realm.getName());
StringBuilder sb = getHeader(user);
sb.append("Someone just requested to change your Keycloak account's password. ");
sb.append("If this was you, click on the link below to set a new password:\n");
sb.append(uri.toString());
sb.append("\n\nThis link will expire within ").append(TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()));
sb.append(" minutes.\n\n");
sb.append("If you don't want to reset your password, just ignore this message and nothing will be changed.\n");
addFooter(sb);
send(user.getEmail(), "Reset password link", sb.toString());
}
private StringBuilder getHeader(UserModel user) {
StringBuilder sb = new StringBuilder();
sb.append("Hi");
if (user.getFirstName() != null) {
sb.append(" ").append(user.getFirstName());
}
sb.append(",\n\n");
return sb;
}
private void addFooter(StringBuilder sb) {
sb.append("\nThanks,\nThe Keycloak Team");
}
}

View file

@ -347,7 +347,7 @@ public class AuthenticationManager {
private boolean checkEnabled(UserModel user) { private boolean checkEnabled(UserModel user) {
if (!user.isEnabled()) { if (!user.isEnabled()) {
logger.warn("Account is disabled, contact admin. " + user.getLoginName()); logger.warn("AccountProvider is disabled, contact admin. " + user.getLoginName());
return false; return false;
} else { } else {
return true; return true;

View file

@ -25,14 +25,16 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.account.Account;
import org.keycloak.account.AccountLoader;
import org.keycloak.account.AccountPages; import org.keycloak.account.AccountPages;
import org.keycloak.account.AccountProvider;
import org.keycloak.audit.Audit; import org.keycloak.audit.Audit;
import org.keycloak.audit.AuditProvider; import org.keycloak.audit.AuditProvider;
import org.keycloak.audit.Details; import org.keycloak.audit.Details;
import org.keycloak.audit.Event; import org.keycloak.audit.Event;
import org.keycloak.audit.Events; 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.AccountRoles;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.AuthenticationLinkModel; import org.keycloak.models.AuthenticationLinkModel;
@ -44,8 +46,8 @@ import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.provider.ProviderSession; import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; 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.SocialLoader;
import org.keycloak.social.SocialProvider; import org.keycloak.social.SocialProvider;
import org.keycloak.social.SocialProviderException; 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.Consumes;
import javax.ws.rs.GET; 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.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
@ -111,8 +109,6 @@ public class AccountService {
AUDIT_DETAILS.add(Details.AUTH_METHOD); AUDIT_DETAILS.add(Details.AUTH_METHOD);
} }
public static final String KEYCLOAK_ACCOUNT_IDENTITY_COOKIE = "KEYCLOAK_ACCOUNT_IDENTITY";
private RealmModel realm; private RealmModel realm;
@Context @Context
@ -131,7 +127,7 @@ public class AccountService {
private final ApplicationModel application; private final ApplicationModel application;
private Audit audit; private Audit audit;
private final SocialRequestManager socialRequestManager; private final SocialRequestManager socialRequestManager;
private Account account; private AccountProvider account;
private Auth auth; private Auth auth;
private AuditProvider auditProvider; private AuditProvider auditProvider;
@ -146,7 +142,7 @@ public class AccountService {
public void init() { public void init() {
auditProvider = providers.getProvider(AuditProvider.class); auditProvider = providers.getProvider(AuditProvider.class);
account = AccountLoader.load().createAccount(uriInfo).setRealm(realm); account = providers.getProvider(AccountProvider.class).setRealm(realm).setUriInfo(uriInfo);
boolean passwordUpdateSupported = false; boolean passwordUpdateSupported = false;
AuthenticationManager.AuthResult authResult = authManager.authenticateRequest(realm, uriInfo, headers); AuthenticationManager.AuthResult authResult = authManager.authenticateRequest(realm, uriInfo, headers);
@ -181,7 +177,7 @@ public class AccountService {
try { try {
require(AccountRoles.MANAGE_ACCOUNT); require(AccountRoles.MANAGE_ACCOUNT);
} catch (ForbiddenException e) { } 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(); String[] referrer = getReferrer();

View file

@ -28,7 +28,9 @@ import org.keycloak.audit.Audit;
import org.keycloak.audit.Details; import org.keycloak.audit.Details;
import org.keycloak.audit.Errors; import org.keycloak.audit.Errors;
import org.keycloak.audit.Events; 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.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -41,13 +43,12 @@ import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.provider.ProviderSession; import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ClientConnection; 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.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.Urls;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.authentication.AuthenticationProviderException; import org.keycloak.authentication.AuthenticationProviderException;
import org.keycloak.authentication.AuthenticationProviderManager; 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.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.Providers;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -120,7 +121,7 @@ public class RequiredActionsService {
String error = Validation.validateUpdateProfileForm(formData); String error = Validation.validateUpdateProfileForm(formData);
if (error != null) { 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")); user.setFirstName(formData.getFirst("firstName"));
@ -160,7 +161,7 @@ public class RequiredActionsService {
String totp = formData.getFirst("totp"); String totp = formData.getFirst("totp");
String totpSecret = formData.getFirst("totpSecret"); 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)) { if (Validation.isEmpty(totp)) {
return loginForms.setError(Messages.MISSING_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP); return loginForms.setError(Messages.MISSING_TOTP).createResponse(RequiredAction.CONFIGURE_TOTP);
} else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) { } else if (!new TimeBasedOTP().validate(totp, totpSecret.getBytes())) {
@ -201,7 +202,7 @@ public class RequiredActionsService {
String passwordNew = formData.getFirst("password-new"); String passwordNew = formData.getFirst("password-new");
String passwordConfirm = formData.getFirst("password-confirm"); 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)) { if (Validation.isEmpty(passwordNew)) {
return loginForms.setError(Messages.MISSING_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD); return loginForms.setError(Messages.MISSING_PASSWORD).createResponse(RequiredAction.UPDATE_PASSWORD);
} else if (!passwordNew.equals(passwordConfirm)) { } else if (!passwordNew.equals(passwordConfirm)) {
@ -261,7 +262,7 @@ public class RequiredActionsService {
initAudit(accessCode); initAudit(accessCode);
//audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); //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); .createResponse(RequiredAction.VERIFY_EMAIL);
} }
} }
@ -277,9 +278,9 @@ public class RequiredActionsService {
return unauthorized(); 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 { } 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); ClientModel client = realm.findClient(clientId);
if (client == null) { 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."); "Unknown login requester.");
} }
if (!client.isEnabled()) { 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."); "Login requester not enabled.");
} }
@ -334,15 +335,22 @@ public class RequiredActionsService {
accessCode.setUsername(username); accessCode.setUsername(username);
try { 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(); audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getId()).success();
} catch (EmailException e) { } catch (EmailException e) {
logger.error("Failed to send password reset email", 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) { private AccessCodeEntry getAccessCodeEntry(RequiredAction requiredAction) {
@ -399,7 +407,7 @@ public class RequiredActionsService {
Set<RequiredAction> requiredActions = user.getRequiredActions(); Set<RequiredAction> requiredActions = user.getRequiredActions();
if (!requiredActions.isEmpty()) { 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()); .createResponse(requiredActions.iterator().next());
} else { } else {
logger.debugv("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri()); logger.debugv("redirectOauth: redirecting to: {0}", accessCode.getRedirectUri());
@ -410,13 +418,13 @@ public class RequiredActionsService {
UserSessionModel session = realm.getUserSession(accessCode.getSessionState()); UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
if (!AuthenticationManager.isSessionValid(realm, session)) { if (!AuthenticationManager.isSessionValid(realm, session)) {
AuthenticationManager.logout(realm, session, uriInfo); 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.session(session);
audit.success(); 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()); session, accessCode.getState(), accessCode.getRedirectUri());
} }
} }
@ -437,7 +445,7 @@ public class RequiredActionsService {
} }
private Response unauthorized() { private Response unauthorized() {
return Flows.forms(realm, uriInfo).setError("Unauthorized request").createErrorPage(); return Flows.forms(providerSession, realm, uriInfo).setError("Unauthorized request").createErrorPage();
} }
} }

View file

@ -96,6 +96,9 @@ public class SocialResource {
ResourceContext resourceContext; ResourceContext resourceContext;
*/ */
@Context
protected ProviderSession providerSession;
@Context @Context
protected KeycloakSession session; protected KeycloakSession session;
@ -133,7 +136,7 @@ public class SocialResource {
.detail(Details.AUTH_METHOD, "social@" + provider.getId()); .detail(Details.AUTH_METHOD, "social@" + provider.getId());
AuthenticationManager authManager = new AuthenticationManager(providers); 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()) { if (!realm.isEnabled()) {
audit.error(Errors.REALM_DISABLED); audit.error(Errors.REALM_DISABLED);
@ -177,7 +180,7 @@ public class SocialResource {
queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, responseType); queryParms.putSingle(OAuth2Constants.RESPONSE_TYPE, responseType);
audit.error(Errors.REJECTED_BY_USER); 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) { } catch (SocialProviderException e) {
logger.error("Failed to process social callback", e); logger.error("Failed to process social callback", e);
return oauth.forwardToSecurityFailure("Failed to process social callback"); return oauth.forwardToSecurityFailure("Failed to process social callback");
@ -279,25 +282,25 @@ public class SocialResource {
SocialProvider provider = SocialLoader.load(providerId); SocialProvider provider = SocialLoader.load(providerId);
if (provider == null) { if (provider == null) {
audit.error(Errors.SOCIAL_PROVIDER_NOT_FOUND); 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); ClientModel client = realm.findClient(clientId);
if (client == null) { if (client == null) {
audit.error(Errors.CLIENT_NOT_FOUND); audit.error(Errors.CLIENT_NOT_FOUND);
logger.warn("Unknown login requester: " + clientId); 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()) { if (!client.isEnabled()) {
audit.error(Errors.CLIENT_DISABLED); audit.error(Errors.CLIENT_DISABLED);
logger.warn("Login requester not enabled."); 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); redirectUri = TokenService.verifyRedirectUri(uriInfo, redirectUri, client);
if (redirectUri == null) { if (redirectUri == null) {
audit.error(Errors.INVALID_REDIRECT_URI); 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 { try {
@ -308,7 +311,7 @@ public class SocialResource {
.putClientAttribute("responseType", responseType).redirectToSocialProvider(); .putClientAttribute("responseType", responseType).redirectToSocialProvider();
} catch (Throwable t) { } catch (Throwable t) {
logger.error("Failed to redirect to social auth", 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();
} }
} }

View file

@ -1,14 +1,17 @@
package org.keycloak.services.resources; package org.keycloak.services.resources;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.Theme; 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.FileTypeMap;
import javax.activation.MimetypesFileTypeMap; import javax.activation.MimetypesFileTypeMap;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.InputStream; import java.io.InputStream;
@ -22,11 +25,15 @@ public class ThemeResource {
private static FileTypeMap mimeTypes = MimetypesFileTypeMap.getDefaultFileTypeMap(); private static FileTypeMap mimeTypes = MimetypesFileTypeMap.getDefaultFileTypeMap();
@Context
private ProviderSession providerSession;
@GET @GET
@Path("/{themType}/{themeName}/{path:.*}") @Path("/{themType}/{themeName}/{path:.*}")
public Response getResource(@PathParam("themType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) { public Response getResource(@PathParam("themType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) {
try { 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); InputStream resource = theme.getResourceAsStream(path);
if (resource != null) { if (resource != null) {
return Response.ok(resource).type(mimeTypes.getContentType(path)).build(); return Response.ok(resource).type(mimeTypes.getContentType(path)).build();
@ -39,5 +46,4 @@ public class ThemeResource {
} }
} }
} }

View file

@ -242,14 +242,14 @@ public class TokenService {
case ACTIONS_REQUIRED: case ACTIONS_REQUIRED:
err = new HashMap<String, String>(); err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, "invalid_grant"); 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); audit.error(Errors.USER_TEMPORARILY_DISABLED);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build(); .build();
case ACCOUNT_DISABLED: case ACCOUNT_DISABLED:
err = new HashMap<String, String>(); err = new HashMap<String, String>();
err.put(OAuth2Constants.ERROR, "invalid_grant"); 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); audit.error(Errors.USER_DISABLED);
return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err) return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(err)
.build(); .build();
@ -340,7 +340,7 @@ public class TokenService {
audit.detail(Details.REMEMBER_ME, "true"); 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()) { if (!checkSsl()) {
return oauth.forwardToSecurityFailure("HTTPS required"); 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); return oauth.processAccessCode(scopeParam, state, redirect, client, user, session, username, remember, "form", audit);
case ACCOUNT_TEMPORARILY_DISABLED: case ACCOUNT_TEMPORARILY_DISABLED:
audit.error(Errors.USER_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: case ACCOUNT_DISABLED:
audit.error(Errors.USER_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: case MISSING_TOTP:
return Flows.forms(realm, uriInfo).setFormData(formData).createLoginTotp(); return Flows.forms(providerSession, realm, uriInfo).setFormData(formData).createLoginTotp();
case INVALID_USER: case INVALID_USER:
audit.error(Errors.USER_NOT_FOUND); 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: default:
audit.error(Errors.INVALID_USER_CREDENTIALS); 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.EMAIL, email)
.detail(Details.REGISTER_METHOD, "form"); .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()) { if (!realm.isEnabled()) {
logger.warn("Realm not enabled"); logger.warn("Realm not enabled");
@ -477,7 +477,7 @@ public class TokenService {
if (error != null) { if (error != null) {
audit.error(Errors.INVALID_REGISTRATION); 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); 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 // Validate that user with this username doesn't exist in realm or any authentication provider
if (realm.getUser(username) != null || authenticationProviderManager.getUser(username) != null) { if (realm.getUser(username) != null || authenticationProviderManager.getUser(username) != null) {
audit.error(Errors.USERNAME_IN_USE); 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); UserModel user = realm.addUser(username);
@ -513,7 +513,7 @@ public class TokenService {
// User already registered, but force him to update password // User already registered, but force him to update password
if (!passwordUpdateSuccessful) { if (!passwordUpdateSuccessful) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); 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"); 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()) { if (!checkSsl()) {
return oauth.forwardToSecurityFailure("HTTPS required"); return oauth.forwardToSecurityFailure("HTTPS required");
@ -766,7 +766,7 @@ public class TokenService {
return oauth.redirectError(client, "access_denied", state, redirect); return oauth.redirectError(client, "access_denied", state, redirect);
} }
logger.info("createLogin() now..."); logger.info("createLogin() now...");
return Flows.forms(realm, uriInfo).createLogin(); return Flows.forms(providerSession, realm, uriInfo).createLogin();
} }
@Path("registrations") @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"); 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()) { if (!checkSsl()) {
return oauth.forwardToSecurityFailure("HTTPS required"); return oauth.forwardToSecurityFailure("HTTPS required");
@ -816,7 +816,7 @@ public class TokenService {
authManager.expireIdentityCookie(realm, uriInfo); authManager.expireIdentityCookie(realm, uriInfo);
return Flows.forms(realm, uriInfo).createRegistration(); return Flows.forms(providerSession, realm, uriInfo).createRegistration();
} }
@Path("logout") @Path("logout")
@ -868,7 +868,7 @@ public class TokenService {
public Response processOAuth(final MultivaluedMap<String, String> formData) { public Response processOAuth(final MultivaluedMap<String, String> formData) {
audit.event(Events.LOGIN).detail(Details.RESPONSE_TYPE, "code"); 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()) { if (!checkSsl()) {
return oauth.forwardToSecurityFailure("HTTPS required"); return oauth.forwardToSecurityFailure("HTTPS required");

View file

@ -6,8 +6,9 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeLoader; import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -280,7 +281,8 @@ public class AdminConsole {
try { try {
//logger.info("getting resource: " + path + " uri: " + uriInfo.getRequestUri().toString()); //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); InputStream resource = theme.getResourceAsStream(path);
if (resource != null) { if (resource != null) {
String contentType = mimeTypes.getContentType(path); String contentType = mimeTypes.getContentType(path);

View file

@ -133,7 +133,7 @@ public class RealmAdminResource {
@Path("users") @Path("users")
public UsersResource users() { public UsersResource users() {
UsersResource users = new UsersResource(realm, auth, tokenManager); UsersResource users = new UsersResource(providers, realm, auth, tokenManager);
ResteasyProviderFactory.getInstance().injectProperties(users); ResteasyProviderFactory.getInstance().injectProperties(users);
//resourceContext.initResource(users); //resourceContext.initResource(users);
return users; return users;

View file

@ -4,6 +4,8 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException; 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.ApplicationModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -15,6 +17,7 @@ import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.adapters.action.UserStats; import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.representations.idm.ApplicationMappingsRepresentation; import org.keycloak.representations.idm.ApplicationMappingsRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; 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.SocialLinkRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation; 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.AccessCodeEntry;
import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.ModelToRepresentation;
import org.keycloak.services.managers.RealmManager; 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.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -53,6 +55,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -63,10 +66,12 @@ public class UsersResource {
protected RealmModel realm; protected RealmModel realm;
private ProviderSession providerSession;
private RealmAuth auth; private RealmAuth auth;
private TokenManager tokenManager; 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.auth = auth;
this.realm = realm; this.realm = realm;
this.tokenManager = tokenManager; this.tokenManager = tokenManager;
@ -660,7 +665,7 @@ public class UsersResource {
ClientModel client = realm.findClient(clientId); ClientModel client = realm.findClient(clientId);
if (client == null || !client.isEnabled()) { 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()); Set<UserModel.RequiredAction> requiredActions = new HashSet<UserModel.RequiredAction>(user.getRequiredActions());
@ -671,7 +676,14 @@ public class UsersResource {
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
try { 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(); return Response.ok().build();
} catch (EmailException e) { } catch (EmailException e) {
logger.error("Failed to send password reset email", e); logger.error("Failed to send password reset email", e);

View file

@ -22,9 +22,9 @@
package org.keycloak.services.resources.flows; package org.keycloak.services.resources.flows;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.login.LoginForms; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.login.LoginFormsLoader;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderSession;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.SocialRequestManager; import org.keycloak.services.managers.SocialRequestManager;
import org.keycloak.services.managers.TokenManager; import org.keycloak.services.managers.TokenManager;
@ -40,13 +40,13 @@ public class Flows {
private Flows() { private Flows() {
} }
public static LoginForms forms(RealmModel realm, UriInfo uriInfo) { public static LoginFormsProvider forms(ProviderSession session, RealmModel realm, UriInfo uriInfo) {
return LoginFormsLoader.load().createForms(realm, 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) { 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) { public static SocialRedirectFlows social(SocialRequestManager socialRequestManager, RealmModel realm, UriInfo uriInfo, SocialProvider provider) {

View file

@ -35,6 +35,7 @@ import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AccessCodeEntry; import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
@ -56,6 +57,8 @@ public class OAuthFlows {
private static final Logger log = Logger.getLogger(OAuthFlows.class); private static final Logger log = Logger.getLogger(OAuthFlows.class);
private final ProviderSession providerSession;
private final RealmModel realm; private final RealmModel realm;
private final HttpRequest request; private final HttpRequest request;
@ -66,8 +69,9 @@ public class OAuthFlows {
private final TokenManager tokenManager; 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) { TokenManager tokenManager) {
this.providerSession = providerSession;
this.realm = realm; this.realm = realm;
this.request = request; this.request = request;
this.uriInfo = uriInfo; this.uriInfo = uriInfo;
@ -84,7 +88,7 @@ public class OAuthFlows {
String code = accessCode.getCode(); String code = accessCode.getCode();
if (Constants.INSTALLED_APP_URN.equals(redirect)) { 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 { } else {
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code); UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, code);
log.debugv("redirectAccessCode: state: {0}", state); log.debugv("redirectAccessCode: state: {0}", state);
@ -102,7 +106,7 @@ public class OAuthFlows {
public Response redirectError(ClientModel client, String error, String state, String redirect) { public Response redirectError(ClientModel client, String error, String state, String redirect) {
if (Constants.INSTALLED_APP_URN.equals(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 { } else {
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, error); UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.ERROR, error);
if (state != null) { if (state != null) {
@ -139,14 +143,14 @@ public class OAuthFlows {
audit.clone().event(Events.SEND_VERIFY_EMAIL).detail(Details.EMAIL, accessCode.getUser().getEmail()).success(); 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); .createResponse(action);
} }
if (!isResource if (!isResource
&& (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) { && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) {
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction()); 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()). setAccessRequest(accessCode.getRealmRolesRequested(), accessCode.getResourceRolesRequested()).
setClient(client).createOAuthGrant(); setClient(client).createOAuthGrant();
} }
@ -160,7 +164,7 @@ public class OAuthFlows {
} }
public Response forwardToSecurityFailure(String message) { 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) { private void isTotpConfigurationRequired(UserModel user) {

View file

@ -1,87 +0,0 @@
package org.keycloak.services.email;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.util.JsonSerialization;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.SocketException;
import java.util.HashMap;
import java.util.UUID;
public class EmailSenderTest {
private GreenMail greenMail;
private EmailSender emailSender;
@Test
public void testUUID() throws Exception{
System.out.println(UUID.randomUUID());
HashMap<String,String> config = new HashMap<String, String>();
config.put("from", "auto@keycloak.org");
config.put("host", "localhost");
config.put("port", "3025");
System.out.println(JsonSerialization.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(config));
}
@Before
public void before() {
ServerSetup setup = new ServerSetup(3025, "localhost", "smtp");
greenMail = new GreenMail(setup);
greenMail.start();
HashMap<String,String> config = new HashMap<String, String>();
config.put("from", "auto@keycloak.org");
config.put("host", "localhost");
config.put("port", "3025");
emailSender = new EmailSender(config);
}
@After
public void after() throws InterruptedException {
if (greenMail != null) {
// Suppress error from GreenMail on shutdown
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
if (!(e.getCause() instanceof SocketException && t.getClass().getName()
.equals("com.icegreen.greenmail.smtp.SmtpHandler"))) {
System.err.print("Exception in thread \"" + t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
});
greenMail.stop();
}
}
@Test
public void sendMail() throws Exception {
emailSender.send("test@test.com", "Test subject", "Test body");
MimeMessage[] receivedMessages = greenMail.getReceivedMessages();
Assert.assertEquals(1, receivedMessages.length);
MimeMessage msg = receivedMessages[0];
Assert.assertEquals(1, msg.getFrom().length);
Assert.assertEquals("auto@keycloak.org", msg.getFrom()[0].toString());
Assert.assertEquals("Test subject", msg.getSubject());
Assert.assertEquals("Test body", ((String) msg.getContent()).trim());
}
}

View file

@ -196,6 +196,16 @@
<artifactId>keycloak-forms-common-themes</artifactId> <artifactId>keycloak-forms-common-themes</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </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> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-account-api</artifactId> <artifactId>keycloak-account-api</artifactId>

View file

@ -20,6 +20,18 @@
"dir": "${keycloak.theme.dir}" "dir": "${keycloak.theme.dir}"
}, },
"login-forms": {
"provider": "freemarker"
},
"account": {
"provider": "freemarker"
},
"email": {
"provider": "freemarker"
},
"scheduled": { "scheduled": {
"interval": 900 "interval": 900
} }

View file

@ -33,6 +33,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient; 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;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
@ -113,12 +114,7 @@ public class RequiredActionEmailVerificationTest {
MimeMessage message = greenMail.getReceivedMessages()[0]; MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent(); String body = (String) message.getContent();
String verificationUrl = MailUtil.getLink(body);
Pattern p = Pattern.compile("(?s).*(http://[^\\s]*).*");
Matcher m = p.matcher(body);
m.matches();
String verificationUrl = m.group(1);
Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent(); Event sendEvent = events.expectRequiredAction("send_verify_email").detail("email", "test-user@localhost").assertEvent();
String sessionId = sendEvent.getSessionId(); String sessionId = sendEvent.getSessionId();
@ -152,16 +148,12 @@ public class RequiredActionEmailVerificationTest {
String body = (String) message.getContent(); 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(); Event sendEvent = events.expectRequiredAction("send_verify_email").user(userId).detail("username", "verifyEmail").detail("email", "email").assertEvent();
String sessionId = sendEvent.getSessionId(); String sessionId = sendEvent.getSessionId();
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
String verificationUrl = m.group(1); String verificationUrl = MailUtil.getLink(body);
driver.navigate().to(verificationUrl.trim()); driver.navigate().to(verificationUrl.trim());
@ -194,13 +186,9 @@ public class RequiredActionEmailVerificationTest {
String body = (String) message.getContent(); 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); 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()); driver.navigate().to(verificationUrl.trim());

View file

@ -35,6 +35,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient; 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;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
@ -133,7 +134,7 @@ public class ResetPasswordTest {
MimeMessage message = greenMail.getReceivedMessages()[0]; MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent(); String body = (String) message.getContent();
String changePasswordUrl = body.split("\n")[3]; String changePasswordUrl = MailUtil.getLink(body);
driver.navigate().to(changePasswordUrl.trim()); driver.navigate().to(changePasswordUrl.trim());
@ -205,7 +206,7 @@ public class ResetPasswordTest {
MimeMessage message = greenMail.getReceivedMessages()[0]; MimeMessage message = greenMail.getReceivedMessages()[0];
String body = (String) message.getContent(); 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(); String sessionId = events.expectRequiredAction("send_reset_password").user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();

View file

@ -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);
}
}