diff --git a/test-framework/bom/pom.xml b/test-framework/bom/pom.xml
index 5a5576e8c5a..0c7162e3b08 100755
--- a/test-framework/bom/pom.xml
+++ b/test-framework/bom/pom.xml
@@ -81,6 +81,12 @@
${project.version}
test
+
+ org.keycloak.test
+ keycloak-test-framework-email-server
+ ${project.version}
+ test
+
diff --git a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/ClientConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/ClientConfigBuilder.java
index ad28b807437..2ab1d35401b 100644
--- a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/ClientConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/ClientConfigBuilder.java
@@ -40,6 +40,11 @@ public class ClientConfigBuilder {
return this;
}
+ public ClientConfigBuilder directAccessGrants() {
+ rep.setDirectAccessGrantsEnabled(true);
+ return this;
+ }
+
public ClientRepresentation build() {
return rep;
}
diff --git a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/RealmConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/RealmConfigBuilder.java
index 4fd3bf8d2e9..68ff6482421 100644
--- a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/RealmConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/RealmConfigBuilder.java
@@ -4,6 +4,9 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;
import java.util.Arrays;
+import java.util.EventListener;
+import java.util.LinkedList;
+import java.util.List;
public class RealmConfigBuilder {
@@ -43,6 +46,14 @@ public class RealmConfigBuilder {
return this;
}
+ public RealmConfigBuilder eventsListeners(String... eventListeners) {
+ if (rep.getEventsListeners() == null) {
+ rep.setEventsListeners(new LinkedList<>());
+ }
+ rep.getEventsListeners().addAll(List.of(eventListeners));
+ return this;
+ }
+
public RealmConfigBuilder roles(String... roleNames) {
if (rep.getRoles() == null) {
rep.setRoles(new RolesRepresentation());
diff --git a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/UserConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/UserConfigBuilder.java
index 71055ee1847..4bd714c9a8f 100644
--- a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/UserConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/UserConfigBuilder.java
@@ -37,6 +37,11 @@ public class UserConfigBuilder {
return this;
}
+ public UserConfigBuilder emailVerified() {
+ rep.setEmailVerified(true);
+ return this;
+ }
+
public UserConfigBuilder password(String password) {
rep.setCredentials(Collections.combine(rep.getCredentials(), Representations.toCredential(CredentialRepresentation.PASSWORD, password)));
return this;
diff --git a/test-framework/email-server/pom.xml b/test-framework/email-server/pom.xml
new file mode 100755
index 00000000000..6e4f981e1e1
--- /dev/null
+++ b/test-framework/email-server/pom.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+ keycloak-test-framework-parent
+ org.keycloak.test
+ 999.0.0-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ keycloak-test-framework-email-server
+ Keycloak Test Framework - Email Server extension
+ jar
+ Email server extension for Keycloak Test Framework
+
+
+ 2.1.1
+
+
+
+
+ org.keycloak.test
+ keycloak-test-framework-core
+ ${project.version}
+
+
+ com.icegreen
+ greenmail
+ ${greenmail.version}
+ compile
+
+
+
diff --git a/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/GreenMailSupplier.java b/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/GreenMailSupplier.java
new file mode 100644
index 00000000000..a651784ff3e
--- /dev/null
+++ b/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/GreenMailSupplier.java
@@ -0,0 +1,52 @@
+package org.keycloak.test.framework.mail;
+
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.test.framework.mail.annotations.InjectMailServer;
+import org.keycloak.test.framework.injection.InstanceContext;
+import org.keycloak.test.framework.injection.RequestedInstance;
+import org.keycloak.test.framework.injection.Supplier;
+import org.keycloak.test.framework.realm.ManagedRealm;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class GreenMailSupplier implements Supplier {
+
+ @Override
+ public Class getAnnotationClass() {
+ return InjectMailServer.class;
+ }
+
+ @Override
+ public Class getValueType() {
+ return MailServer.class;
+ }
+
+ @Override
+ public MailServer getValue(InstanceContext instanceContext) {
+ ManagedRealm realm = instanceContext.getDependency(ManagedRealm.class);
+ RealmRepresentation representation = realm.admin().toRepresentation();
+
+ Map config = new HashMap<>();
+ config.put("from", "auto@keycloak.org");
+ config.put("host", "localhost");
+ config.put("port", "3025");
+
+ representation.setSmtpServer(config);
+ realm.admin().update(representation);
+
+ MailServer mailServer = new MailServer();
+ mailServer.start();
+ return mailServer;
+ }
+
+ @Override
+ public void close(InstanceContext instanceContext) {
+ instanceContext.getValue().stop();
+ }
+
+ @Override
+ public boolean compatible(InstanceContext a, RequestedInstance b) {
+ return true;
+ }
+}
diff --git a/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/GreenMailTestFrameworkExtension.java b/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/GreenMailTestFrameworkExtension.java
new file mode 100644
index 00000000000..e596b9b838b
--- /dev/null
+++ b/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/GreenMailTestFrameworkExtension.java
@@ -0,0 +1,15 @@
+package org.keycloak.test.framework.mail;
+
+import org.keycloak.test.framework.TestFrameworkExtension;
+import org.keycloak.test.framework.injection.Supplier;
+
+import java.util.List;
+
+public class GreenMailTestFrameworkExtension implements TestFrameworkExtension {
+
+ @Override
+ public List> suppliers() {
+ return List.of(new GreenMailSupplier());
+ }
+
+}
diff --git a/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/MailServer.java b/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/MailServer.java
new file mode 100644
index 00000000000..e21f6ee7c8f
--- /dev/null
+++ b/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/MailServer.java
@@ -0,0 +1,52 @@
+package org.keycloak.test.framework.mail;
+
+import com.icegreen.greenmail.store.FolderException;
+import com.icegreen.greenmail.util.GreenMail;
+import com.icegreen.greenmail.util.ServerSetup;
+import jakarta.mail.internet.MimeMessage;
+import org.keycloak.test.framework.injection.ManagedTestResource;
+
+public class MailServer extends ManagedTestResource {
+
+ private static final int PORT = 3025;
+ private static final String HOST = "localhost";
+
+ private GreenMail greenMail;
+
+ public void start() {
+ ServerSetup setup = new ServerSetup(PORT, HOST, "smtp");
+
+ greenMail = new GreenMail(setup);
+ greenMail.start();
+ }
+
+ public void stop() {
+ greenMail.stop();
+ }
+
+ public MimeMessage[] getReceivedMessages() {
+ return greenMail.getReceivedMessages();
+ }
+
+ public MimeMessage getLastReceivedMessage() {
+ MimeMessage[] receivedMessages = greenMail.getReceivedMessages();
+ return receivedMessages != null && receivedMessages.length > 0 ? receivedMessages[receivedMessages.length - 1] : null;
+ }
+
+ public boolean waitForIncomingEmail(long timeout, int emailCount) {
+ return greenMail.waitForIncomingEmail(timeout, emailCount);
+ }
+
+ public boolean waitForIncomingEmail(int emailCount) {
+ return greenMail.waitForIncomingEmail(emailCount);
+ }
+
+ @Override
+ public void runCleanup() {
+ try {
+ greenMail.purgeEmailFromAllMailboxes();
+ } catch (FolderException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/annotations/InjectMailServer.java b/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/annotations/InjectMailServer.java
new file mode 100644
index 00000000000..9e4a734b0ec
--- /dev/null
+++ b/test-framework/email-server/src/main/java/org/keycloak/test/framework/mail/annotations/InjectMailServer.java
@@ -0,0 +1,10 @@
+package org.keycloak.test.framework.mail.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface InjectMailServer { }
diff --git a/test-framework/email-server/src/main/resources/META-INF/services/org.keycloak.test.framework.TestFrameworkExtension b/test-framework/email-server/src/main/resources/META-INF/services/org.keycloak.test.framework.TestFrameworkExtension
new file mode 100644
index 00000000000..7108670c459
--- /dev/null
+++ b/test-framework/email-server/src/main/resources/META-INF/services/org.keycloak.test.framework.TestFrameworkExtension
@@ -0,0 +1 @@
+org.keycloak.test.framework.mail.GreenMailTestFrameworkExtension
\ No newline at end of file
diff --git a/test-framework/examples/tests/pom.xml b/test-framework/examples/tests/pom.xml
index ebdacdcb31e..69bf85b57e5 100644
--- a/test-framework/examples/tests/pom.xml
+++ b/test-framework/examples/tests/pom.xml
@@ -72,6 +72,10 @@
org.keycloak.test
keycloak-test-framework-oauth-nimbus-poc
+
+ org.keycloak.test
+ keycloak-test-framework-email-server
+
org.keycloak.test
keycloak-test-framework-ui
diff --git a/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/EmailTest.java b/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/EmailTest.java
new file mode 100644
index 00000000000..f2ecdc2db32
--- /dev/null
+++ b/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/EmailTest.java
@@ -0,0 +1,71 @@
+package org.keycloak.test.examples;
+
+import com.nimbusds.oauth2.sdk.GeneralException;
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.MimeMessage;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.keycloak.events.email.EmailEventListenerProviderFactory;
+import org.keycloak.test.framework.annotations.InjectRealm;
+import org.keycloak.test.framework.annotations.InjectUser;
+import org.keycloak.test.framework.annotations.KeycloakIntegrationTest;
+import org.keycloak.test.framework.mail.MailServer;
+import org.keycloak.test.framework.mail.annotations.InjectMailServer;
+import org.keycloak.test.framework.oauth.nimbus.OAuthClient;
+import org.keycloak.test.framework.oauth.nimbus.annotations.InjectOAuthClient;
+import org.keycloak.test.framework.realm.ManagedRealm;
+import org.keycloak.test.framework.realm.ManagedUser;
+import org.keycloak.test.framework.realm.RealmConfig;
+import org.keycloak.test.framework.realm.RealmConfigBuilder;
+import org.keycloak.test.framework.realm.UserConfig;
+import org.keycloak.test.framework.realm.UserConfigBuilder;
+
+import java.io.IOException;
+import java.util.Map;
+
+@KeycloakIntegrationTest
+public class EmailTest {
+
+ @InjectRealm(config = EmailSenderRealmConfig.class)
+ ManagedRealm realm;
+
+ @InjectUser(config = UserWithEmail.class)
+ ManagedUser user;
+
+ @InjectMailServer
+ MailServer mail;
+
+ @InjectOAuthClient
+ OAuthClient oAuthClient;
+
+ @Test
+ public void testEmail() throws GeneralException, IOException, MessagingException {
+ oAuthClient.resourceOwnerCredentialGrant(user.getUsername(), "invalid");
+
+ Map smtpServer = realm.admin().toRepresentation().getSmtpServer();
+ Assertions.assertEquals("auto@keycloak.org", smtpServer.get("from"));
+ Assertions.assertEquals("localhost", smtpServer.get("host"));
+ Assertions.assertEquals("3025", smtpServer.get("port"));
+
+ mail.waitForIncomingEmail(1);
+ MimeMessage lastReceivedMessage = mail.getLastReceivedMessage();
+ Assertions.assertEquals("Login error", lastReceivedMessage.getSubject());
+ }
+
+ public static class EmailSenderRealmConfig implements RealmConfig {
+
+ @Override
+ public RealmConfigBuilder configure(RealmConfigBuilder realm) {
+ return realm.eventsListeners(EmailEventListenerProviderFactory.ID);
+ }
+ }
+
+ public static class UserWithEmail implements UserConfig {
+
+ @Override
+ public UserConfigBuilder configure(UserConfigBuilder user) {
+ return user.username("test").email("test@local").password("password").emailVerified();
+ }
+ }
+
+}
diff --git a/test-framework/oauth-nimbus-poc/src/main/java/org/keycloak/test/framework/oauth/nimbus/DefaultOAuthClientConfiguration.java b/test-framework/oauth-nimbus-poc/src/main/java/org/keycloak/test/framework/oauth/nimbus/DefaultOAuthClientConfiguration.java
index 245cdb835b9..531df143eab 100644
--- a/test-framework/oauth-nimbus-poc/src/main/java/org/keycloak/test/framework/oauth/nimbus/DefaultOAuthClientConfiguration.java
+++ b/test-framework/oauth-nimbus-poc/src/main/java/org/keycloak/test/framework/oauth/nimbus/DefaultOAuthClientConfiguration.java
@@ -9,6 +9,7 @@ public class DefaultOAuthClientConfiguration implements ClientConfig {
public ClientConfigBuilder configure(ClientConfigBuilder client) {
return client.clientId("test-oauth-client")
.serviceAccount()
+ .directAccessGrants()
.redirectUris("http://127.0.0.1/callback/oauth")
.secret("test-secret");
}
diff --git a/test-framework/oauth-nimbus-poc/src/main/java/org/keycloak/test/framework/oauth/nimbus/OAuthClient.java b/test-framework/oauth-nimbus-poc/src/main/java/org/keycloak/test/framework/oauth/nimbus/OAuthClient.java
index 73c4fd5878a..29585eb104f 100644
--- a/test-framework/oauth-nimbus-poc/src/main/java/org/keycloak/test/framework/oauth/nimbus/OAuthClient.java
+++ b/test-framework/oauth-nimbus-poc/src/main/java/org/keycloak/test/framework/oauth/nimbus/OAuthClient.java
@@ -6,6 +6,7 @@ import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.AuthorizationRequest;
import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
import com.nimbusds.oauth2.sdk.GeneralException;
+import com.nimbusds.oauth2.sdk.ResourceOwnerPasswordCredentialsGrant;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.TokenIntrospectionRequest;
import com.nimbusds.oauth2.sdk.TokenIntrospectionResponse;
@@ -65,6 +66,15 @@ public class OAuthClient {
return TokenResponse.parse(tokenRequest.toHTTPRequest().send());
}
+ public TokenResponse resourceOwnerCredentialGrant(String username, String password) throws GeneralException, IOException {
+ ResourceOwnerPasswordCredentialsGrant credentialsGrant = new ResourceOwnerPasswordCredentialsGrant(username, new Secret(password));
+ ClientAuthentication clientAuthentication = getClientAuthentication();
+ URI tokenEndpoint = getOIDCProviderMetadata().getTokenEndpointURI();
+
+ TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuthentication, credentialsGrant);
+ return TokenResponse.parse(tokenRequest.toHTTPRequest().send());
+ }
+
public TokenResponse tokenRequest(AuthorizationCode authorizationCode) throws IOException, GeneralException {
AuthorizationGrant grant = new AuthorizationCodeGrant(authorizationCode, callbackServer.getRedirectionUri());
ClientAuthentication clientAuthentication = getClientAuthentication();
diff --git a/test-framework/pom.xml b/test-framework/pom.xml
index 4c0ef7beb5c..fbff53bfb31 100755
--- a/test-framework/pom.xml
+++ b/test-framework/pom.xml
@@ -40,6 +40,7 @@
db-mysql
db-oracle
db-postgres
+ email-server
oauth-nimbus-poc
ui
examples