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>
<para>
The Account SPI allows implementing the account management pages using whatever web framework or templating
engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProvider</literal>
and <literal>org.keycloak.account.Account</literal> in <literal>forms/account-api</literal>.
engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProviderFactory</literal>
and <literal>org.keycloak.account.AccountProvider</literal> in <literal>forms/account-api</literal>.
</para>
<para>
Keycloaks default account management provider is built on the FreeMarker template engine (<literal>forms/account-freemarker</literal>).
To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-account-freemarker-1.0-beta-1-SNAPSHOT.jar</literal>
or disable it with the system property <literal>org.keycloak.account.freemarker.FreeMarkerAccountProvider</literal>.
or disable it with the system property <literal>org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory</literal>.
</para>
</section>
<section>
<title>Login SPI</title>
<para>
The Login SPI allows implementing the login forms using whatever web framework or templating
engine you want. To create a Login forms provider implement <literal>org.keycloak.login.LoginFormsProvider</literal>
and <literal>org.keycloak.login.LoginForms</literal> in <literal>forms/login-api</literal>.
engine you want. To create a Login forms provider implement <literal>org.keycloak.login.LoginFormsProviderFactory</literal>
and <literal>org.keycloak.login.LoginFormsProvider</literal> in <literal>forms/login-api</literal>.
</para>
<para>
Keycloaks default login forms provider is built on the FreeMarker template engine (<literal>forms/login-freemarker</literal>).
To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-login-freemarker-1.0-beta-1-SNAPSHOT.jar</literal>
or disable it with the system property <literal>org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider</literal>.
or disable it with the system property <literal>org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory</literal>.
</para>
</section>
</section>

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;
import org.keycloak.audit.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.Provider;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface AccountProvider {
public interface AccountProvider extends Provider {
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;
import org.keycloak.account.Account;
import org.jboss.logging.Logger;
import org.keycloak.account.AccountPages;
import org.keycloak.account.AccountProvider;
import org.keycloak.account.freemarker.model.AccountBean;
import org.keycloak.account.freemarker.model.AccountSocialBean;
import org.keycloak.account.freemarker.model.FeaturesBean;
import org.keycloak.account.freemarker.model.LogBean;
import org.keycloak.account.freemarker.model.MessageBean;
import org.keycloak.account.freemarker.model.ReferrerBean;
import org.keycloak.account.freemarker.model.SessionsBean;
import org.keycloak.account.freemarker.model.TotpBean;
import org.keycloak.account.freemarker.model.UrlBean;
import org.keycloak.audit.Event;
import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderSession;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerAccountProvider implements AccountProvider {
private static final Logger logger = Logger.getLogger(FreeMarkerAccountProvider.class);
private UserModel user;
private Response.Status status = Response.Status.OK;
private RealmModel realm;
private String[] referrer;
private List<Event> events;
private List<UserSessionModel> sessions;
private boolean social;
private boolean audit;
private boolean passwordUpdateSupported;
private ProviderSession session;
public static enum MessageType {SUCCESS, WARNING, ERROR}
private UriInfo uriInfo;
private String message;
private MessageType messageType;
public FreeMarkerAccountProvider(ProviderSession session) {
this.session = session;
}
public AccountProvider setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
return this;
}
@Override
public Account createAccount(UriInfo uriInfo) {
return new FreeMarkerAccount(uriInfo);
public Response createResponse(AccountPages page) {
Map<String, Object> attributes = new HashMap<String, Object>();
ExtendingThemeManager themeManager = new ExtendingThemeManager(session);
Theme theme;
try {
theme = themeManager.createTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT);
} catch (IOException e) {
logger.error("Failed to create theme", e);
return Response.serverError().build();
}
try {
attributes.put("properties", theme.getProperties());
} catch (IOException e) {
logger.warn("Failed to load properties", e);
}
Properties messages;
try {
messages = theme.getMessages();
attributes.put("rb", messages);
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messages = new Properties();
}
URI baseUri = uriInfo.getBaseUri();
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
for (Map.Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
baseUriBuilder.queryParam(e.getKey(), e.getValue().toArray());
}
URI baseQueryUri = baseUriBuilder.build();
if (message != null) {
attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType));
}
if (referrer != null) {
attributes.put("referrer", new ReferrerBean(referrer));
}
attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri()));
attributes.put("features", new FeaturesBean(social, audit, passwordUpdateSupported));
switch (page) {
case ACCOUNT:
attributes.put("account", new AccountBean(user));
break;
case TOTP:
attributes.put("totp", new TotpBean(user, baseUri));
break;
case SOCIAL:
attributes.put("social", new AccountSocialBean(realm, user, uriInfo.getBaseUri()));
break;
case LOG:
attributes.put("log", new LogBean(events));
break;
case SESSIONS:
attributes.put("sessions", new SessionsBean(realm, sessions));
break;
}
try {
String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme);
return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
return Response.serverError().build();
}
}
@Override
public AccountProvider setError(String message) {
this.message = message;
this.messageType = MessageType.ERROR;
return this;
}
@Override
public AccountProvider setSuccess(String message) {
this.message = message;
this.messageType = MessageType.SUCCESS;
return this;
}
@Override
public AccountProvider setWarning(String message) {
this.message = message;
this.messageType = MessageType.WARNING;
return this;
}
@Override
public AccountProvider setUser(UserModel user) {
this.user = user;
return this;
}
@Override
public AccountProvider setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
@Override
public AccountProvider setStatus(Response.Status status) {
this.status = status;
return this;
}
@Override
public AccountProvider setReferrer(String[] referrer) {
this.referrer = referrer;
return this;
}
@Override
public AccountProvider setEvents(List<Event> events) {
this.events = events;
return this;
}
@Override
public AccountProvider setSessions(List<UserSessionModel> sessions) {
this.sessions = sessions;
return this;
}
@Override
public AccountProvider setFeatures(boolean social, boolean audit, boolean passwordUpdateSupported) {
this.social = social;
this.audit = audit;
this.passwordUpdateSupported = passwordUpdateSupported;
return this;
}
@Override
public 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;
import org.keycloak.account.freemarker.FreeMarkerAccount;
import org.keycloak.account.freemarker.FreeMarkerAccountProvider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -30,9 +30,9 @@ public class MessageBean {
private String summary;
private FreeMarkerAccount.MessageType type;
private FreeMarkerAccountProvider.MessageType type;
public MessageBean(String message, FreeMarkerAccount.MessageType type) {
public MessageBean(String message, FreeMarkerAccountProvider.MessageType type) {
this.summary = message;
this.type = type;
}
@ -46,15 +46,15 @@ public class MessageBean {
}
public boolean isSuccess() {
return FreeMarkerAccount.MessageType.SUCCESS.equals(this.type);
return FreeMarkerAccountProvider.MessageType.SUCCESS.equals(this.type);
}
public boolean isWarning() {
return FreeMarkerAccount.MessageType.WARNING.equals(this.type);
return FreeMarkerAccountProvider.MessageType.WARNING.equals(this.type);
}
public boolean isError() {
return FreeMarkerAccount.MessageType.ERROR.equals(this.type);
return FreeMarkerAccountProvider.MessageType.ERROR.equals(this.type);
}
}

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;
import org.keycloak.Config;
import org.keycloak.util.ProviderLoader;
import org.keycloak.provider.ProviderSession;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Properties;
import java.util.Set;
/**
* @author <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 {
if (name == null) {
name = Config.scope("theme").get("default");
}
private List<ThemeProvider> providers;
private String defaultTheme;
List<ThemeProvider> providers = new LinkedList();
for (ThemeProvider p : ProviderLoader.load(ThemeProvider.class)) {
providers.add(p);
public ExtendingThemeManager(ProviderSession providerSession) {
providers = new LinkedList();
for (ThemeProvider p : providerSession.getAllProviders(ThemeProvider.class)) {
if (!p.getClass().equals(ExtendingThemeManager.class)) {
providers.add(p);
}
}
Collections.sort(providers, new Comparator<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) {
List<Theme> themes = new LinkedList<Theme>();
themes.add(theme);
if (theme.getImportName() != null) {
String[] s = theme.getImportName().split("/");
themes.add(findTheme(providers, s[1], Theme.Type.valueOf(s[0].toUpperCase())));
themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase())));
}
for (String parentName = theme.getParentName(); parentName != null; parentName = theme.getParentName()) {
theme = findTheme(providers, parentName, type);
theme = findTheme(parentName, type);
themes.add(theme);
if (theme.getImportName() != null) {
String[] s = theme.getImportName().split("/");
themes.add(findTheme(providers, s[1], Theme.Type.valueOf(s[0].toUpperCase())));
themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase())));
}
}
@ -61,7 +79,31 @@ public class ThemeLoader {
}
}
private static Theme findTheme(Iterable<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) {
if (p.hasTheme(name, type)) {
try {

View file

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

View file

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

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

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

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>
@ -9,4 +9,7 @@ public class EmailException extends Exception {
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;
import org.keycloak.models.RealmModel;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface LoginFormsProvider {
public LoginForms createForms(RealmModel realm, UriInfo uriInfo);
}
package org.keycloak.login;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setRealm(RealmModel realm);
public LoginFormsProvider setUriInfo(UriInfo uriInfo);
public Response createResponse(UserModel.RequiredAction action);
public Response createLogin();
public Response createPasswordReset();
public Response createLoginTotp();
public Response createRegistration();
public Response createErrorPage();
public Response createOAuthGrant();
public Response createCode();
public LoginFormsProvider setAccessCode(String accessCodeId, String accessCode);
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
public LoginFormsProvider setError(String message);
public LoginFormsProvider setSuccess(String message);
public LoginFormsProvider setWarning(String message);
public LoginFormsProvider setUser(UserModel user);
public LoginFormsProvider setClient(ClientModel client);
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams);
public LoginFormsProvider setFormData(MultivaluedMap<String, String> formData);
public LoginFormsProvider setStatus(Response.Status status);
}

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

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;
import org.keycloak.login.LoginForms;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.RealmModel;
import javax.ws.rs.core.UriInfo;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
@Override
public LoginForms createForms(RealmModel realm, UriInfo uriInfo) {
return new FreeMarkerLoginForms(realm, uriInfo);
}
}
package org.keycloak.login.freemarker;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.login.LoginFormsPages;
import org.keycloak.login.freemarker.model.CodeBean;
import org.keycloak.login.freemarker.model.LoginBean;
import org.keycloak.login.freemarker.model.MessageBean;
import org.keycloak.login.freemarker.model.OAuthGrantBean;
import org.keycloak.login.freemarker.model.ProfileBean;
import org.keycloak.login.freemarker.model.RealmBean;
import org.keycloak.login.freemarker.model.RegisterBean;
import org.keycloak.login.freemarker.model.SocialBean;
import org.keycloak.login.freemarker.model.TotpBean;
import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderSession;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.flows.Urls;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class);
private String message;
private String accessCodeId;
private String accessCode;
private Response.Status status = Response.Status.OK;
private List<RoleModel> realmRolesRequested;
private MultivaluedMap<String, RoleModel> resourceRolesRequested;
private MultivaluedMap<String, String> queryParams;
public static enum MessageType {SUCCESS, WARNING, ERROR}
private MessageType messageType = MessageType.ERROR;
private MultivaluedMap<String, String> formData;
private ProviderSession session;
private RealmModel realm;
private UserModel user;
private ClientModel client;
private UriInfo uriInfo;
public FreeMarkerLoginFormsProvider(ProviderSession session) {
this.session = session;
}
public LoginFormsProvider setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
public LoginFormsProvider setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
return this;
}
public Response createResponse(UserModel.RequiredAction action) {
String actionMessage;
LoginFormsPages page;
switch (action) {
case CONFIGURE_TOTP:
actionMessage = Messages.ACTION_WARN_TOTP;
page = LoginFormsPages.LOGIN_CONFIG_TOTP;
break;
case UPDATE_PROFILE:
actionMessage = Messages.ACTION_WARN_PROFILE;
page = LoginFormsPages.LOGIN_UPDATE_PROFILE;
break;
case UPDATE_PASSWORD:
actionMessage = Messages.ACTION_WARN_PASSWD;
page = LoginFormsPages.LOGIN_UPDATE_PASSWORD;
break;
case VERIFY_EMAIL:
try {
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
builder.queryParam("key", accessCodeId);
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
session.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendVerifyEmail(link, expiration);
} catch (EmailException e) {
logger.error("Failed to send verification email", e);
return setError("emailSendError").createErrorPage();
}
actionMessage = Messages.ACTION_WARN_EMAIL;
page = LoginFormsPages.LOGIN_VERIFY_EMAIL;
break;
default:
return Response.serverError().build();
}
if (message == null) {
setWarning(actionMessage);
}
return createResponse(page);
}
private Response createResponse(LoginFormsPages page) {
MultivaluedMap<String, String> queryParameterMap = queryParams != null ? queryParams : uriInfo.getQueryParameters();
String requestURI = uriInfo.getBaseUri().getPath();
UriBuilder uriBuilder = UriBuilder.fromUri(requestURI);
for (String k : queryParameterMap.keySet()) {
Object[] objects = queryParameterMap.get(k).toArray();
if (objects.length == 1 && objects[0] == null) continue; //
uriBuilder.replaceQueryParam(k, objects);
}
if (accessCode != null) {
uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode);
}
Map<String, Object> attributes = new HashMap<String, Object>();
ExtendingThemeManager themeManager = new ExtendingThemeManager(session);
Theme theme;
try {
theme = themeManager.createTheme(realm.getLoginTheme(), Theme.Type.LOGIN);
} catch (IOException e) {
logger.error("Failed to create theme", e);
return Response.serverError().build();
}
try {
attributes.put("properties", theme.getProperties());
} catch (IOException e) {
logger.warn("Failed to load properties", e);
}
Properties messages;
try {
messages = theme.getMessages();
attributes.put("rb", messages);
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messages = new Properties();
}
if (message != null) {
attributes.put("message", new MessageBean(messages.containsKey(message) ? messages.getProperty(message) : message, messageType));
}
if (page == LoginFormsPages.OAUTH_GRANT) {
// for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
uriBuilder.replaceQuery(null);
}
URI baseUri = uriBuilder.build();
if (realm != null) {
attributes.put("realm", new RealmBean(realm));
attributes.put("social", new SocialBean(realm, baseUri));
attributes.put("url", new UrlBean(realm, theme, baseUri));
}
attributes.put("login", new LoginBean(formData));
switch (page) {
case LOGIN_CONFIG_TOTP:
attributes.put("totp", new TotpBean(realm, user, baseUri));
break;
case LOGIN_UPDATE_PROFILE:
attributes.put("user", new ProfileBean(user));
break;
case REGISTER:
attributes.put("register", new RegisterBean(formData));
break;
case OAUTH_GRANT:
attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested));
break;
case CODE:
attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null));
break;
}
try {
String result = FreeMarkerUtil.processTemplate(attributes, Templates.getTemplate(page), theme);
return Response.status(status).type(MediaType.TEXT_HTML).entity(result).build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
return Response.serverError().build();
}
}
public Response createLogin() {
return createResponse(LoginFormsPages.LOGIN);
}
public Response createPasswordReset() {
return createResponse(LoginFormsPages.LOGIN_RESET_PASSWORD);
}
public Response createLoginTotp() {
return createResponse(LoginFormsPages.LOGIN_TOTP);
}
public Response createRegistration() {
return createResponse(LoginFormsPages.REGISTER);
}
public Response createErrorPage() {
setStatus(Response.Status.INTERNAL_SERVER_ERROR);
return createResponse(LoginFormsPages.ERROR);
}
public Response createOAuthGrant() {
return createResponse(LoginFormsPages.OAUTH_GRANT);
}
@Override
public Response createCode() {
return createResponse(LoginFormsPages.CODE);
}
public FreeMarkerLoginFormsProvider setError(String message) {
this.message = message;
this.messageType = MessageType.ERROR;
return this;
}
public FreeMarkerLoginFormsProvider setSuccess(String message) {
this.message = message;
this.messageType = MessageType.SUCCESS;
return this;
}
public FreeMarkerLoginFormsProvider setWarning(String message) {
this.message = message;
this.messageType = MessageType.WARNING;
return this;
}
public FreeMarkerLoginFormsProvider setUser(UserModel user) {
this.user = user;
return this;
}
public FreeMarkerLoginFormsProvider setClient(ClientModel client) {
this.client = client;
return this;
}
public FreeMarkerLoginFormsProvider setFormData(MultivaluedMap<String, String> formData) {
this.formData = formData;
return this;
}
@Override
public LoginFormsProvider setAccessCode(String accessCodeId, String accessCode) {
this.accessCodeId = accessCodeId;
this.accessCode = accessCode;
return this;
}
@Override
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested) {
this.realmRolesRequested = realmRolesRequested;
this.resourceRolesRequested = resourceRolesRequested;
return this;
}
@Override
public LoginFormsProvider setStatus(Response.Status status) {
this.status = status;
return this;
}
@Override
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams) {
this.queryParams = queryParams;
return this;
}
@Override
public void close() {
}
}

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

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>account-api</module>
<module>account-freemarker</module>
<module>email-api</module>
<module>email-freemarker</module>
<module>login-api</module>
<module>login-freemarker</module>
</modules>

View file

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

View file

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

View file

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

View file

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

View file

@ -49,6 +49,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-email-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-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) {
if (!user.isEnabled()) {
logger.warn("Account is disabled, contact admin. " + user.getLoginName());
logger.warn("AccountProvider is disabled, contact admin. " + user.getLoginName());
return false;
} else {
return true;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,8 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
@ -15,6 +17,7 @@ import org.keycloak.models.SocialLinkModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.representations.idm.ApplicationMappingsRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
@ -23,8 +26,6 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.SocialLinkRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.email.EmailException;
import org.keycloak.services.email.EmailSender;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.ModelToRepresentation;
import org.keycloak.services.managers.RealmManager;
@ -46,6 +47,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.HashMap;
@ -53,6 +55,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -63,10 +66,12 @@ public class UsersResource {
protected RealmModel realm;
private ProviderSession providerSession;
private RealmAuth auth;
private TokenManager tokenManager;
public UsersResource(RealmModel realm, RealmAuth auth, TokenManager tokenManager) {
public UsersResource(ProviderSession providerSession, RealmModel realm, RealmAuth auth, TokenManager tokenManager) {
this.providerSession = providerSession;
this.auth = auth;
this.realm = realm;
this.tokenManager = tokenManager;
@ -660,7 +665,7 @@ public class UsersResource {
ClientModel client = realm.findClient(clientId);
if (client == null || !client.isEnabled()) {
return Flows.errors().error("Account management not enabled", Response.Status.INTERNAL_SERVER_ERROR);
return Flows.errors().error("AccountProvider management not enabled", Response.Status.INTERNAL_SERVER_ERROR);
}
Set<UserModel.RequiredAction> requiredActions = new HashSet<UserModel.RequiredAction>(user.getRequiredActions());
@ -671,7 +676,14 @@ public class UsersResource {
accessCode.setExpiration(Time.currentTime() + realm.getAccessCodeLifespanUserAction());
try {
new EmailSender(realm.getSmtpConfig()).sendPasswordReset(user, realm, accessCode, uriInfo);
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
builder.queryParam("key", accessCode.getId());
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
providerSession.getProvider(EmailProvider.class).setRealm(realm).setUser(user).sendPasswordReset(link, expiration);
return Response.ok().build();
} catch (EmailException e) {
logger.error("Failed to send password reset email", e);

View file

@ -22,9 +22,9 @@
package org.keycloak.services.resources.flows;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.login.LoginForms;
import org.keycloak.login.LoginFormsLoader;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderSession;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.SocialRequestManager;
import org.keycloak.services.managers.TokenManager;
@ -40,13 +40,13 @@ public class Flows {
private Flows() {
}
public static LoginForms forms(RealmModel realm, UriInfo uriInfo) {
return LoginFormsLoader.load().createForms(realm, uriInfo);
public static LoginFormsProvider forms(ProviderSession session, RealmModel realm, UriInfo uriInfo) {
return session.getProvider(LoginFormsProvider.class).setRealm(realm).setUriInfo(uriInfo);
}
public static OAuthFlows oauth(RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
public static OAuthFlows oauth(ProviderSession session, RealmModel realm, HttpRequest request, UriInfo uriInfo, AuthenticationManager authManager,
TokenManager tokenManager) {
return new OAuthFlows(realm, request, uriInfo, authManager, tokenManager);
return new OAuthFlows(session, realm, request, uriInfo, authManager, tokenManager);
}
public static SocialRedirectFlows social(SocialRequestManager socialRequestManager, RealmModel realm, UriInfo uriInfo, SocialProvider provider) {

View file

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

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>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-email-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-email-freemarker</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-account-api</artifactId>

View file

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

View file

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

View file

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

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