Before diving in, make sure you have the following prerequisites in place:
- Java Development Setup: You should have Java installed.
- Maven: We will use maven for building the Keycloak extension.
- Docker: We will use docker to spin up a development container quickly.
- Mailtrap Account or Email server: Sign up for a free Mailtrap account. We’ll use Mailtrap to test that the emails are sent correctly without needing a real SMTP server. You can use your own email server if you prefer.
Preparing Keycloak Environment for SPI Development
To get started with developing and testing the SPI, we first need to set up the Keycloak environment and configure it to use a mail server. For simplicity, we’ll use Mailtrap, but feel free to use your own mail server if you have one.
Step 1: Set Up a Mailtrap Inbox
- Create an account on Mailtrap and create a new inbox. This will allow us to easily test the email functionality.
- Once the inbox is set up, navigate to
Integrations -> SMTP
and take note of the following details:Host
,Port
,Username
, andPassword
. We’ll need these values to configure Keycloak’s SMTP server settings later.
Step 2: Start Keycloak in a Docker Container
Next, we’ll start Keycloak in a Docker container along with a PostgreSQL database. To do this, create a docker-compose.yaml
file in the root directory of your project with the following content:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
keycloak:
image: quay.io/keycloak/keycloak:25.0.2
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
KC_HOSTNAME: "http://localhost:8080"
KC_HTTP_ENABLED: true
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
depends_on:
- postgres
ports:
- "8080:8080"
command:
- start-dev
volumes:
postgres_data:
To start the containers, run the following command in your terminal:
docker-compose up
This will pull the necessary images, start PostgreSQL, and launch Keycloak in development mode.
Step 3: Create a New Realm and Configure SMTP
Once the container is running we obtain an access token by authenticating with the Keycloak Admin API. make sure you have curl
and jq
installed
Once the Keycloak container is running, we need to authenticate with the Keycloak Admin API and obtain an access token. Make sure you have curl and jq installed, then run the following command:
TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin" \
-d "password=admin" \
-d "grant_type=password" \
-d "client_id=admin-cli" | jq -r '.access_token')
This command authenticates with the Keycloak Admin API and stores the access token in the TOKEN
variable for future API requests.
With the access token, we can now create a new realm and configure it to use the Mailtrap SMTP server. Run the following command, making sure to replace YOUR-MAILTRAP-USERNAME
and YOUR-MAILTRAP-PASSWORD
with the SMTP credentials from Mailtrap:
curl -X POST "http://localhost:8080/admin/realms" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"realm": "dev",
"enabled": true,
"displayName": "Test Realm",
"sslRequired": "none",
"registrationAllowed": true,
"smtpServer": {
"host": "smtp.mailtrap.io",
"port": "25",
"from": "[email protected]",
"auth": true,
"user": "YOUR-MAILTRAP-USERNAME",
"password": "YOUR-MAILTRAP-PASSWORD",
"ssl": false,
"starttls": true
}
}'
This command creates a new realm called dev and configures the Mailtrap SMTP server to handle email sending for this realm.
Building a Custom SPI to Send Welcome Emails
Now that the Keycloak environment is ready, we can start building the custom Service Provider Interface (SPI) to send a welcome email to users after registration. We’ll begin by creating a Maven project to structure our code and implement the logic for sending emails.
To create the Maven project, run the following command:
mvn archetype:generate -DgroupId=com.adelhub.kc \
-DartifactId=welcome-mail-listener \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
After running the Maven command, your project structure will look like this:
├── docker-compose.yaml
└── welcome-mail-listener
├── pom.xml
└── src
├── main
│ └── java
│ └── com
│ └── adelhub
│ └── kc
│ └── App.java
└── test
└── java
└── com
└── adelhub
└── kc
└── AppTest.java
Edit your pom.xml file to add the following depndencies
<properties>
<keycloak.version>25.0.2</keycloak.version>
</properties>
<dependencies>
<!-- Pebble template engine -->
<dependency>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble</artifactId>
<version>3.2.2</version>
</dependency>
<!-- SLF4J API for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.10</version>
</dependency>
<!-- Keycloak Server SPI (provided by Keycloak runtime) -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<!-- Keycloak Server SPI Private (provided by Keycloak runtime) -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<!-- Keycloak Services (provided by Keycloak runtime) -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
Since we are using a third-party depndency pebble
for templating which is not a part of the Keycloak Quarkus runtime we need to instruct maven to package this depndency
into the generated JAR. we achieve this using the maven-shade-plugin
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
This is very important. Otherwise we will recieve java.lang.ClassNotFoundException
Next, we need to create the HTML and text templates for the emails. We will be using Pebble Templates
, a lightweight and fast templating engine, to generate these email templates dynamically based on user data.
We begin by creating two files: one for the HTML version and one for the plain-text version of the welcome email in the resources directory(src/main/resources
).
cd welcome-mail-listener/
mkdir -p src/main/resources/templates
touch src/main/resources/templates/welcome-email.html
touch src/main/resources/templates/welcome-email.txt
For example we can populate the files as follows:
welcome-email.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Welcome to Our Service!</title>
</head>
<body>
<h1>Welcome to Our Service!</h1>
<p>Dear {{ firstName }},</p>
<p>
We're glad to have you. Please let us know if you need any assistance.
</p>
<p>Best regards,<br />The Team</p>
</body>
</html>
welcome-email.txt
:
Welcome to Our Service!
Dear {{ firstName }},
We're glad to have you. Please let us know if you need any assistance.
Best regards,
The Team
After setting up the templates, the next step is to create two classes that will handle the event listener logic: WelcomeEmailEventListenerProviderFactory
and WelcomeEmailEventListenerProvider
.
First, create the WelcomeEmailEventListenerProvider
class. This class will handle user registration events and trigger a welcome email when a new user registers.
rm src/main/java/com/adelhub/kc/App.java
touch src/main/java/com/adelhub/kc/WelcomeEmailEventListenerProvider.java
To achieve this, we need to implement Keycloak’s EventListenerProvider
interface. By implementing this interface, we can hook into Keycloak’s event system and define custom logic for each event type (such as user registrations, logins, or administrative actions). Let’s take a look at the interface definition:
public interface EventListenerProvider extends Provider {
/**
*
* Called when a user event occurs e.g. log in, register.
* <p/>
* Note this method should not do any action that cannot be rolled back, see {@link EventListenerProvider} javadoc
* for more details.
*
* @param event to be triggered
*/
void onEvent(Event event);
/**
*
* Called when an admin event occurs e.g. a client was updated/deleted.
* <p/>
* Note this method should not do any action that cannot be rolled back, see {@link EventListenerProvider} javadoc
* for more details.
*
* @param event to be triggered
* @param includeRepresentation when false, event listener should NOT include representation field in the resulting
* action
*/
void onEvent(AdminEvent event, boolean includeRepresentation);
As you can see the EventListenerProvider
interface has two main methods:
onEvent(Event event)
: Handles user-related events (e.g., registration, login).onEvent(AdminEvent event, boolean includeRepresentation)
: Handles admin-specific events
We will hook into both types of events and send a Welcome email whenever a new user registers or a new user has been created by an admin. Let’s write the basic structure of WelcomeEmailEventListenerProvider
package com.adelhub.kc;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailSenderProvider;
import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
public class WelcomeEmailEventListenerProvider implements EventListenerProvider {
private static final Logger logger = LoggerFactory.getLogger(WelcomeEmailEventListenerProvider.class);
private final KeycloakSession session;
// The Template engine we will use to dynamically evaluate our email templates
private static final PebbleEngine pebbleEngine = new PebbleEngine.Builder().build();
public static PebbleTemplate htmlTemplate; // Our HTML Email template
public static PebbleTemplate textTemplate; // Our plain-text Email template
// Static block to load templates
static {
try {
// Initialize Pebble templates
htmlTemplate = pebbleEngine.getTemplate("templates/welcome-email.html");
textTemplate = pebbleEngine.getTemplate("templates/welcome-email.txt");
} catch (Exception e) {
logger.error("Error loading email templates", e);
}
}
public WelcomeEmailEventListenerProvider(KeycloakSession session) {
this.session = session;
}
@Override
public void onEvent(Event event) {
// Event handling logic will go here
}
@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
// Admin Event handling logic will go here
}
@Override
public void close() {
// Cleanup code if necessary
}
}
Next, we modify the onEvent
methods to listen for the user registration event
@Override
public void onEvent(Event event) {
logger.info("Event received: {}", event.getType());
if (event.getType().equals(EventType.REGISTER)) {
logger.info("User registered with ID: {}", event.getUserId());
UserModel user = session.users().getUserById(session.getContext().getRealm(), event.getUserId());
sendWelcomeEmail(user); // We will implement this next
}
}
@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
if (event.getResourceType() == ResourceType.USER && event.getOperationType() == OperationType.CREATE) {
String userId = event.getResourcePath().split("/")[1];
logger.info("User registered with ID: {}", userId);
UserModel user = session.users().getUserById(session.getContext().getRealm(), userId);
if(user != null) {
sendWelcomeEmail(user); // We will implement this next
}
}
}
Then we create the sendWelcomeEmail
method, which will load the HTML and text templates, and render them with user-specific data
private void sendWelcomeEmail(UserModel user) {
String firstName = user.getFirstName();
// Prepare data for the template
Map<String, Object> context = new HashMap<>();
context.put("firstName", firstName);
StringWriter htmlWriter = new StringWriter();
StringWriter textWriter = new StringWriter();
try {
// Render the templates using Pebble
htmlTemplate.evaluate(htmlWriter, context);
textTemplate.evaluate(textWriter, context);
} catch (Exception e) {
logger.error("Error evaluating templates", e);
return;
}
String htmlBody = htmlWriter.toString();
String textBody = textWriter.toString();
String subject = "Welcome to our Awesome App";
sendEmail(user, subject, htmlBody, textBody); // we will implement this method next
}
and the sendEmail
method:
private void sendEmail(UserModel user, String subject, String htmlBody, String textBody) {
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
try {
emailSender.send(session.getContext().getRealm().getSmtpConfig(), user, subject, textBody, htmlBody);
} catch (EmailException e) {
logger.error("Error sending email", e);
}
}
Now our WelcomeEmailEventListenerProvider
class should look like:
package com.adelhub.kc;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailSenderProvider;
import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.keycloak.events.admin.ResourceType;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
public class WelcomeEmailEventListenerProvider implements EventListenerProvider {
private static final Logger logger = LoggerFactory.getLogger(WelcomeEmailEventListenerProvider.class);
private final KeycloakSession session;
// Pebble engine and templates, loaded once statically
private static final PebbleEngine pebbleEngine = new PebbleEngine.Builder().build();
private static PebbleTemplate htmlTemplate;
private static PebbleTemplate textTemplate;
// Static block to load templates only once
static {
try {
// Initialize Pebble templates
htmlTemplate = pebbleEngine.getTemplate("templates/welcome-email.html");
textTemplate = pebbleEngine.getTemplate("templates/welcome-email.txt");
} catch (Exception e) {
logger.error("Error loading email templates", e);
}
}
public WelcomeEmailEventListenerProvider(KeycloakSession session) {
this.session = session;
}
@Override
public void onEvent(Event event) {
logger.info("Event received: {}", event.getType());
if (event.getType().equals(EventType.REGISTER)) {
logger.info("User registered with ID: {}", event.getUserId());
UserModel user = session.users().getUserById(session.getContext().getRealm(), event.getUserId());
sendWelcomeEmail(user);
}
}
@Override
public void onEvent(AdminEvent event, boolean includeRepresentation) {
if (event.getResourceType() == ResourceType.USER && event.getOperationType() == OperationType.CREATE) {
String userId = event.getResourcePath().split("/")[1];
logger.info("User registered with ID: {}", userId);
UserModel user = session.users().getUserById(session.getContext().getRealm(), userId);
if(user != null) {
sendWelcomeEmail(user);
}
}
}
@Override
public void close() {
}
private void sendWelcomeEmail(UserModel user) {
String firstName = user.getFirstName();
// Prepare data for the template
Map<String, Object> context = new HashMap<>();
context.put("firstName", firstName);
StringWriter htmlWriter = new StringWriter();
StringWriter textWriter = new StringWriter();
try {
// Render the templates using Pebble
htmlTemplate.evaluate(htmlWriter, context);
textTemplate.evaluate(textWriter, context);
} catch (Exception e) {
logger.error("Error evaluating templates", e);
return;
}
String htmlBody = htmlWriter.toString();
String textBody = textWriter.toString();
String subject = "Welcome to our Awesome App";
// Send the email
sendEmail(user, subject, htmlBody, textBody);
}
private void sendEmail(UserModel user, String subject, String htmlBody, String textBody) {
EmailSenderProvider emailSender = session.getProvider(EmailSenderProvider.class);
try {
emailSender.send(session.getContext().getRealm().getSmtpConfig(), user, subject, textBody, htmlBody);
} catch (EmailException e) {
logger.error("Error sending email", e);
}
}
}
Next, To register our custom WelcomeEmailEventListenerProvider
in Keycloak, you need to implement a factory class. Keycloak uses the factory design pattern to create instances of providers. Thus we will create WelcomeEmailEventListenerProviderFactory
rm src/main/java/com/adelhub/kc/App.java
touch src/main/java/com/adelhub/kc/WelcomeEmailEventListenerProviderFactory.java
The WelcomeEmailEventListenerProviderFactory
will implement the EventListenerProviderFactory
which itself extends ProviderFactory<EventListenerProvider>
Let’s take a quick look at the Interface
public interface EventListenerProviderFactory extends ProviderFactory<EventListenerProvider> {
}
public interface ProviderFactory<T extends Provider> {
T create(KeycloakSession session);
/**
* Only called once when the factory is first created. This config is pulled from keycloak_server.json
*
* @param config
*/
void init(Config.Scope config);
/**
* Called after all provider factories have been initialized
*/
void postInit(KeycloakSessionFactory factory);
/**
* This is called when the server shuts down.
*
*/
void close();
String getId();
default int order() {
return 0;
}
/**
* Returns the metadata for each configuration property supported by this factory.
*
* @return a list with the metadata for each configuration property supported by this factory
*/
default List<ProviderConfigProperty> getConfigMetadata() {
return Collections.emptyList();
}
}
As you can see, the ProviderFactory
interface defines several methods to manage the lifecycle of a provider, including initialization, creation, and cleanup. However, for our purposes in WelcomeEmailEventListenerProviderFactory
, the most important methods are:
create(KeycloakSession session)
: for creating and returning an instance of our customWelcomeEmailEventListenerProvider
and supplying it with the current keycloak session.getId()
: provides a unique identifier for our event listener, which Keycloak uses to register and reference the listener.
We can now implement our factory.
package com.adelhub.kc;
import org.keycloak.Config;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WelcomeEmailEventListenerProviderFactory implements EventListenerProviderFactory {
private static final Logger logger = LoggerFactory.getLogger(WelcomeEmailEventListenerProvider.class);
@Override
public EventListenerProvider create(KeycloakSession session) {
return new WelcomeEmailEventListenerProvider(session);
}
@Override
public void init(Config.Scope config) {
logger.info("Initialized welcome email provider");
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// Post Init logic goes here
}
@Override
public void close() {
// Clean up logic goes here
}
@Override
public String getId() {
// Important: ID is how Keycloak identifies our custom provider
return "welcome-email-provider";
}
}
Registering the Provider in Keycloak
Once the factory is created, we need to let Keycloak know about it. To do this, you must create a META-INF/services
directory and register the factory class.
mkdir -p src/main/resources/META-INF/services
touch src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory
Now we need to add the Fully Qualified Name of the Factory Class
echo "com.adelhub.kc.WelcomeEmailEventListenerProviderFactory" > src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory
This tells Keycloak that your factory is available and should be used to create instances of the WelcomeEmailEventListenerProvider
.
After performing the above steps, your directory structure should look like this
├── docker-compose.yaml
└── welcome-mail-listener
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── adelhub
│ │ │ └── kc
│ │ │ └── WelcomeEmailEventListenerProvider.java
│ │ └── resources
│ │ ├── META-INF
│ │ │ └── services
│ │ │ └── org.keycloak.events.EventListenerProviderFactory
│ │ └── templates
│ │ ├── welcome-email.html
│ │ └── welcome-email.txt
│ └── test
│ └── java
│ └── com
│ └── adelhub
│ └── kc
└── target
├── classes
│ ├── META-INF
│ │ └── services
│ │ └── org.keycloak.events.EventListenerProviderFactory
│ ├── com
│ │ └── adelhub
│ │ └── kc
│ │ └── WelcomeEmailEventListenerProvider.class
│ └── templates
│ ├── welcome-email.html
│ └── welcome-email.txt
└── test-classes
└── com
└── adelhub
└── kc
where the org.keycloak.events.EventListenerProviderFactory
file contains the fully qualified name of the factory class you created.
Building and Deploying the Welcome Email SPI
Once you’ve implemented both the factory and the event listener provider, build your project using Maven:
mvn clean package
This will package your SPI into a JAR file.
target
├── classes
├── generated-sources
├── generated-test-sources
├── maven-archiver
├── maven-status
├── test-classes
└── welcome-mail-listener-1.0-SNAPSHOT.jar
We now need to copy this JAR file into the Keycloak providers
directory. Since we are using docker compose we will do that using a docker volume. edit your compose file to add
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
keycloak:
image: quay.io/keycloak/keycloak:25.0.2
volumes: # Copy the JAR File into the providers directory inside of keycloak
- ./welcome-mail-listener/target/welcome-mail-listener-1.0-SNAPSHOT.jar:/opt/keycloak/providers/welcome-mail-listener.jar
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: password
KC_HOSTNAME: "http://localhost:8080"
KC_HTTP_ENABLED: true
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
depends_on:
- postgres
ports:
- "8080:8080"
command:
- start-dev
volumes:
postgres_data:
After restarting the docker compose stack you should see something like the below in the container logs
keycloak-1 | 2024-09-06 12:45:03,463 WARN [org.key.services] (build-26) KC-SERVICES0047: welcome-email-provider (com.adelhub.kc.WelcomeEmailEventListenerProviderFactory) is implementing the internal SPI eventsListener. This SPI is internal and may change without notice
keycloak-1 | 2024-09-06 12:45:05,262 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 2458ms
keycloak-1 | 2024-09-06 12:45:06,051 INFO [com.adelhub.kc.WelcomeEmailEventListenerProvider] (main) Initialized welcome email provider
which indicates that keycloak loaded the SPI.
Although Keycloak has now loaded the welcome-email-listener
SPI, we still need to enable it for the realm. While this can be done via the UI by going to Realm Settings → Events
and adding the listener to the Event Listeners field, we will enable it using the API for consistency.
Step 1: Get Get an Admin Access Token
TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin" \
-d "password=admin" \
-d "grant_type=password" \
-d "client_id=admin-cli" | jq -r '.access_token')
Step 2: Enable the Custom Event Listener Now that we have the access token, we can enable the custom welcome-email-listener for the realm.
curl -X PUT "http://localhost:8080/admin/realms/dev" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"eventsListeners": ["jboss-logging", "welcome-email-listener"],
"eventsEnabled": true
}'
Replace dev
in “http://localhost:8080/admin/realms/dev” with your realm name in case you choose to create a realm with a different name.
Verifying the Welcome Email Listener
With the welcome-email-listener
SPI built, deployed, and enabled, it’s time to verify its functionality. We will confirm that a welcome email is triggered upon both user registration through the admin API and direct registration through the Keycloak account console.
Step 1: Obtain a New Admin Token
First, obtain a new admin access token in case the previous one has expired by rerunning the above command
Step 2: Register a New User via Admin API
You can create a new user using the admin API with the following Curl request
curl -X POST "http://localhost:8080/admin/realms/dev/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "bob",
"enabled": true,
"firstName": "Bob",
"lastName": "Marley",
"email": "[email protected]",
"credentials": [
{
"type": "password",
"value": "strongpassword",
"temporary": false
}
]
}'
Step 3: Register a New User via the Keycloak Account Console
You can also trigger the welcome email by registering a new user directly from the Keycloak account console. Follow these steps:
- Open the Keycloak UI by navigating to http://localhost:8080/auth/realms/dev/account/.
- Click Register to create a new user.
- Complete the registration form with details like username, email, and password, then submit the form.
Step 4: Check Your Email
After registering the user via either method (API or UI), go to your inbox on Mailtrap to check for the welcome email. You should find a message sent from [email protected]
to the newly registered user’s email address (e.g., [email protected]
).
Here’s a screenshot of the received email:
Wrapping Up
In this post, we explored how to extend Keycloak with a custom SPI to automatically send welcome emails to new users. Whether users are created through the admin API or register via the Keycloak account console.
Now, this is just a simple example, and admittedly, it’s not the most optimized solution (for instance, we’re sending the emails synchronously). There’s definitely room for improvement but I hope the main ideas were clear.
You can check out all the code for this project on GitHub:
If you’ve got any feedback, questions or suggestions, don’t hesitate to reach out to me!