commit
3be4579204
75 changed files with 1513 additions and 1041 deletions
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.account.AccountSpi
|
|
@ -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;
|
||||
}
|
||||
}
|
202
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
Normal file → Executable file
202
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
Normal file → Executable 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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
org.keycloak.account.freemarker.FreeMarkerAccountProvider
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory
|
|
@ -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 {
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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> {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.freemarker.ThemeSpi
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
org.keycloak.theme.DefaultKeycloakThemeProvider
|
||||
org.keycloak.theme.FolderThemeProvider
|
|
@ -0,0 +1,2 @@
|
|||
org.keycloak.theme.DefaultKeycloakThemeProviderFactory
|
||||
org.keycloak.theme.FolderThemeProviderFactory
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
|||
emailVerificationSubject=Verify email
|
||||
passwordResetSubject=Reset password
|
|
@ -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
44
forms/email-api/pom.xml
Executable 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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.email.EmailSpi
|
71
forms/email-freemarker/pom.xml
Executable file
71
forms/email-freemarker/pom.xml
Executable 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>
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.email.freemarker.FreeMarkerEmailProviderFactory
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.login.LoginFormsSpi
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory
|
|
@ -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>
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -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>
|
||||
|
|
|
@ -59,4 +59,8 @@ public class AerogearThemeProvider implements ThemeProvider {
|
|||
return nameSet(type).contains(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -20,6 +20,18 @@
|
|||
"dir": "${jboss.server.config.dir}/themes"
|
||||
},
|
||||
|
||||
"login-forms": {
|
||||
"provider": "freemarker"
|
||||
},
|
||||
|
||||
"account": {
|
||||
"provider": "freemarker"
|
||||
},
|
||||
|
||||
"email": {
|
||||
"provider": "freemarker"
|
||||
},
|
||||
|
||||
"scheduled": {
|
||||
"interval": 900
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -20,6 +20,18 @@
|
|||
"dir": "${keycloak.theme.dir}"
|
||||
},
|
||||
|
||||
"login-forms": {
|
||||
"provider": "freemarker"
|
||||
},
|
||||
|
||||
"account": {
|
||||
"provider": "freemarker"
|
||||
},
|
||||
|
||||
"email": {
|
||||
"provider": "freemarker"
|
||||
},
|
||||
|
||||
"scheduled": {
|
||||
"interval": 900
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue