How to create a Keycloak plugin
Tags: keycloak, java, tutorial, webdev
Published: 2022-10-31 12:27:57

## Introduction

[Keycloak](https://www.keycloak.org) is an open-source Identity and Access Management solution. I find it very useful for small to large teams that need to implement their own authentication system but do not have the capacity to develop a secure service themselves. It is written in Java and offers an SPI ([Service Provider Interface](https://en.wikipedia.org/wiki/Service_provider_interface)). This means that it is easily extendable with custom implementations of existing classes and new additions via plugins.

So how do you create a plugin? In this tutorial I would like to give an example of how to develop and test a Keycloak plugin.

I should note that, while every users needs are different, it is highly likely that the plugin you want to develop already exists in [this brilliant repo](https://github.com/thomasdarimont/keycloak-extension-playground) by user [thomasdarimont](https://github.com/thomasdarimont). Make sure to take a look and at least get an inspiration.

In this tutorial we will be developing a plugin that would authenticate users based on a link sent to their email. At the time of writing there are no examples like this present in the forementioned repository. If you are looking for the complete project, visit my GitHub - https://github.com/yakovlev-alexey/keycloak-email-link-auth. Commit history roughly follows this tutorial.

## Table of Contents

-   [Introduction](#introduction)
-   [Table of Contents](#table-of-contents)
-   [Initialize the project](#initialize-the-project)
-   [Create a test bench](#create-a-test-bench)
-   [Implement a custom Authenticator](#implement-a-custom-authenticator)
    -   [Custom Action Token](#custom-action-token)
    -   [Authenticator implementation](#authenticator-implementation)
    -   [Custom resources for plugins](#custom-resources-for-plugins)
    -   [Notify Keycloak of new Providers](#notify-keycloak-of-new-providers)
-   [Configure test bench](#configure-test-bench)
-   [Authenticator configuration](#authenticator-configuration)
-   [Next steps](#next-steps)
-   [Conclusion](#conclusion)

## Initialize the project

First let's create a Maven project. I prefer doing that with a shell command but you may be using your favourite IDE for the same result.

```bash
mvn archetype:generate -DgroupId=dev.yakovlev_alexey -DartifactId=keycloak-email-link-auth-plugin -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
```

> Make sure to enter your own `groupId` and `artifactId`.

This would generate a structure like this

```
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── dev
    │           └── yakovlev_alexey
    │               └── App.java
    └── test
        └── java
            └── dev
                └── yakovlev_alexey
                    └── AppTest.java
```

At this time we are not going to create unit tests although it is entirely possible. So let's remove the `test` folder and `App.java` file. Now we need to install required dependencies to develop our plugin. Make the following modifications to your `pom.xml`

```xml
    <properties>
        <!-- other properties -->
        <keycloak.version>19.0.3</keycloak.version>
    </properties>

    <dependencies>
        <!-- generated by maven - may be removed if you do not plan to unit test -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <!-- HERE GO KEYCLAOK DEPENDENCIES -->
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi-private</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-services</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.keycloak</groupId>
                <artifactId>keycloak-parent</artifactId>
                <version>${keycloak.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
```

All keycloak dependencies are specified as `provided`. According to [documentation](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-scope) this means that Maven will expect the runtime (JDK or in our case Keycloak) to provide those dependencies. This is exactly what we need. Still we need a centralised way to manage Keycloak dependency versions. For this we use [`dependencyManagement` property](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-management). Make sure to run `mvn install` to update your dependencies.

Now we are ready to develop our plugin though it would make sense to first create a test bench where we would be able to quickly test our changes locally without modifiying your actual Keycloak instance or even deploying it.

## Create a test bench

Let's create a test bench using Docker and docker-compose. I recommend using docker-compose from the start since it's highly likely that your plugin is going to have _some_ external dependency like an SMTP server. Using docker-compose would allow you to later effortlessly add other services to create a truly isolated environment. I use the following `docker-compose.yaml`:

```yaml
# ./docker/docker-compose.yaml
version: "3.2"

services:
    keycloak:
        build:
            context: ./
            dockerfile: Dockerfile
            args:
                - KEYCLOAK_IMAGE=${KEYCLOAK_IMAGE}
        environment:
            KEYCLOAK_ADMIN: admin
            KEYCLOAK_ADMIN_PASSWORD: admin
            DB_VENDOR: h2
        volumes:
            - ./h2:/opt/keycloak/data/h2
        ports:
            - "8024:8080"
```

And the following `Dockerfile`:

```Dockerfile
# ./docker/Dockerfile
ARG KEYCLOAK_IMAGE

FROM $KEYCLOAK_IMAGE

USER root
COPY plugins/*.jar /opt/keycloak/providers/
USER 1000

ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start-dev"]
```

In order for docker-compose to work we need a few environment variables. Specify them in `.env` file.

```xml
<!-- ./docker/.env -->
COMPOSE_PROJECT_NAME=keycloak
KEYCLOAK_IMAGE=quay.io/keycloak/keycloak:19.0.3
```

Create `plugins` folder inside `docker` directory. This is where `jar` files with compiled plugins will go.

```
docker
├── .env
├── Dockerfile
├── docker-compose.yaml
├── h2
└── plugins
	└── .gitkeep
```

Make sure to ignore `h2` folder and `plugins` contents in your VCS. Your `.gitignore` might look something like this:

```bash
# ./.gitignore
target
docker/h2

docker/plugins/*
!docker/plugins/.gitkeep
```

Finally you can run `docker-compose up --build keycloak` in your `docker` folder to start your test instance of Keycloak.

## Implement a custom Authenticator

Now back to our goal. We want to authenticate users based on the link we send to their email. To do that we implement a custom [Authenticator](https://www.keycloak.org/docs/latest/server_development/#_auth_spi). Authenticator is basically a step in the process of authenticating a user. It may be anything from a form that requires input from user to complex redirect.

In order to get an idea how to create an authenticator (or any other class you might need that leverages SPI for that matter) I recommend taking a look at Keycloak source code hosted on GitHub - https://github.com/keycloak/keycloak. As you might expect Keycloak has a very large codebase therefore it would be easier to use GitHub search to find the class you might need. In our case you may find `authenticators` directory interesting - https://github.com/keycloak/keycloak/tree/main/services/src/main/java/org/keycloak/authentication/authenticators. I will not go into detail about existing Keycloak authenticators and their implementations and rather start scaffolding our own authenticator.

Another thing I recommend is to follow the same folder structure as Keycloak. Therefore our plugin should look something like this:

```
src
└── main
	└── java
		└── dev
			└── yakovlev_alexey
				└── keycloak
					└── authentication
						└── authenticators
							└── browser
								└── EmailLinkAuthenticator.java
```

Create class `EmailLinkAuthenticator` and implement `Authenticator` interface from `org.keycloak.authentication.Authenticator`. After stubbing required methods your code could look like this:

```java
package dev.yakovlev_alexey.keycloak.authentication.authenticators.browser;

import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;

public class EmailLinkAuthenticator implements Authenticator {

    @Override
    public void close() {
        // TODO Auto-generated method stub
    }

    @Override
    public void action(AuthenticationFlowContext context) {
        // TODO Auto-generated method stub
    }

    @Override
    public void authenticate(AuthenticationFlowContext context) {
        // TODO Auto-generated method stub
    }

    @Override
    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean requiresUser() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
        // TODO Auto-generated method stub
    }

}
```

There is a bunch of methods but we will need only some of them. Important ones are `authenticate` which is called when user enters this Authenticator in the Authentication flow and `action` which is called when user submits the form.

To start with we may specify that this Authenticator requires a user. This basically means that at the point of reaching this authenticator we should already have an idea who the user may want to authenticate as. In simpler terms prior to reaching our authenticator the user should enter their username. In order to show that to Keycloak just return `true` from `requiresUser` method.

`configuredFor` method allows us to tell Keycloak whether we are able to authenticate the user in a certain context. Context being the realm with its settings, Keycloak session and the user being authenticated. In our case we only need the user to have an email. So let's return `user.getEmail() != null` from `configuredFor`.

Now it is time to implement the actual logic for this authenticator. We need it to do a few things:

1. When the authenticator gets called the first time, send an email with a link and then show a "email sent" page
2. Page should have a button to resend the email (in case delivery fails)
3. Page should have a button to submit the verification (e.g. I confirmed the verification in a different browser/device and want to continue in this tab)

The closest authenticator to ours is `VerifyEmail` required action. While it is not really an authenticator it is very similar. [Looking at the implementation](https://github.com/keycloak/keycloak/blob/43a3677cc727469a7c0d46c95436420e0ff3feb5/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java#L131) we see that in order to send an email it creates a token and encodes it in an action token URL.

```java
private Response sendVerifyEmail(KeycloakSession session, LoginFormsProvider forms, UserModel user, AuthenticationSessionModel authSession, EventBuilder event) throws UriBuilderException, IllegalArgumentException {
	RealmModel realm = session.getContext().getRealm();
	UriInfo uriInfo = session.getContext().getUri();

	int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(VerifyEmailActionToken.TOKEN_TYPE);
	int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;

	String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
	VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail(), authSession.getClient().getClientId());
	UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
			authSession.getClient().getClientId(), authSession.getTabId());
	String link = builder.build(realm.getName()).toString();
	long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);

	try {
		session
			.getProvider(EmailTemplateProvider.class)
			.setAuthenticationSession(authSession)
			.setRealm(realm)
			.setUser(user)
			.sendVerifyEmail(link, expirationInMinutes);
		event.success();
	} catch (EmailException e) {
		logger.error("Failed to send verification email", e);
		event.error(Errors.EMAIL_SEND_FAILED);
	}

	return forms.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
}
```

### Custom Action Token

I think it makes sense to copy the implementation except our parameters would be slightly different to account for different contexts in authenticators and requried actions. But to properly use this implementation we need an action token of our own. Action token is a class that represents a JWT. It extends `DefaultActionToken` class and has a corresponding `ActionTokenHandler` class. Tokens can be encoded into a URL to which action token handler will respond. Let's create our own action token by copying `VerifyEmail` one.

`EmailLinkActionToken` should be located in `keycloak.authentication.actiontoken.emaillink` package. Your folder structure should look like the following one:

```
src
└── main
	└── java
		└── dev
			└── yakovlev_alexey
				└── keycloak
					└── authentication
						├── actiontoken
						│   └── emaillink
						│       ├── EmailLinkActionToken.java
						│       └── EmailLinkActionTokenHandler.java
						└── authenticators
							└── browser
								└── EmailLinkAuthenticator.java
```

Let's implement `EmailLinkActionToken` by copying `VerifyEmailActionToken` and replacing the name and removing `originalAuthenticationSessionId`:

```java
package dev.yakovlev_alexey.keycloak.authentication.actiontoken.emaillink;

import org.keycloak.authentication.actiontoken.DefaultActionToken;

public class EmailLinkActionToken extends DefaultActionToken {
    public static final String TOKEN_TYPE = "email-link";

    public EmailLinkActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId,
            String email, String clientId) {
        super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
        this.issuedFor = clientId;

        setEmail(email);
    }

    private EmailLinkActionToken() {
    }
}
```

> As you might notice I removed `originalAuthenticationSessionId`. It is used to create a new authenticaton session by reissuing the token and using its link as the submit action to confirm sign up in a different browser.

Next let's implement the handler for it. This time just copying will not work: contexts are very different. First let's settle on an implementation where visiting the link immediately gives the user consent (unlike the `VerifyEmail` one where manually clicking a button is required). After visiting the link an info page will be shown. To achieve this behaviour we will roughly follow the same procedure as in `VerifyEmailActionTokenHandler` except I will try to comment important parts since the code is not as easy to understand.

```java
package dev.yakovlev_alexey.keycloak.authentication.actiontoken.emaillink;

import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler;
import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.authentication.actiontoken.*;
import org.keycloak.events.*;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionModel;

import dev.yakovlev_alexey.keycloak.authentication.authenticators.browser.EmailLinkAuthenticator;

import java.util.Collections;

import javax.ws.rs.core.Response;

public class EmailLinkActionTokenHandler extends AbstractActionTokenHandler<EmailLinkActionToken> {

    public EmailLinkActionTokenHandler() {
        super(
                EmailLinkActionToken.TOKEN_TYPE,
                EmailLinkActionToken.class,
                Messages.STALE_VERIFY_EMAIL_LINK,
                EventType.VERIFY_EMAIL,
                Errors.INVALID_TOKEN);
    }

    @Override
    public Predicate<? super EmailLinkActionToken>[] getVerifiers(
            ActionTokenContext<EmailLinkActionToken> tokenContext) {
        // this is different to VerifyEmailActionTokenHandler implementation because
        // since its implementation a helper was added
        return TokenUtils.predicates(verifyEmail(tokenContext));
    }

    @Override
    public Response handleToken(EmailLinkActionToken token, ActionTokenContext<EmailLinkActionToken> tokenContext) {
        AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
        UserModel user = authSession.getAuthenticatedUser();
        KeycloakSession session = tokenContext.getSession();
        EventBuilder event = tokenContext.getEvent();
        RealmModel realm = tokenContext.getRealm();

        event.event(EventType.VERIFY_EMAIL)
                .detail(Details.EMAIL, user.getEmail())
                .success();

        // verify user email as we know it is valid as this entry point would never have
        // gotten here
        user.setEmailVerified(true);

        // fresh auth session means that the link was open in a different browser window
        // or device
        if (!tokenContext.isAuthenticationSessionFresh()) {
            // link was opened in the same browser session (session is not fresh) - save the
            // user a click and continue authentication in the new (current) tab
            // previous tab will be thrown away
            String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession,
                    tokenContext.getRequest(), tokenContext.getEvent());
            return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(),
                    authSession, tokenContext.getUriInfo(), nextAction);
        }

        AuthenticationSessionCompoundId compoundId = AuthenticationSessionCompoundId
                .encoded(token.getCompoundAuthenticationSessionId());

        AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
        asm.removeAuthenticationSession(realm, authSession, true);

        ClientModel originalClient = realm.getClientById(compoundId.getClientUUID());
        // find the original authentication session
        // (where the tab is waiting to confirm)
        authSession = asm.getAuthenticationSessionByIdAndClient(realm, compoundId.getRootSessionId(),
                originalClient, compoundId.getTabId());

        if (authSession != null) {
            authSession.setAuthNote(EmailLinkAuthenticator.EMAIL_LINK_VERIFIED, user.getEmail());
        } else {
            // if no session was found in the same instance it might still be in the same
            // cluster if you have multiple replicas of Keycloak
            session.authenticationSessions().updateNonlocalSessionAuthNotes(
                    compoundId,
                    Collections.singletonMap(EmailLinkAuthenticator.EMAIL_LINK_VERIFIED,
                            token.getEmail()));
        }

        // show success page
        return session.getProvider(LoginFormsProvider.class)
                .setAuthenticationSession(authSession)
                .setSuccess(Messages.EMAIL_VERIFIED, token.getEmail())
                .createInfoPage();
    }

	// we do not really want users to authenticate using the same link multiple times
    @Override
    public boolean canUseTokenRepeatedly(EmailLinkActionToken token,
            ActionTokenContext<EmailLinkActionToken> tokenContext) {
        return false;
    }
}
```

> To practice a little yourself try to reimplement this action token and handler to require additional confirmation (like it works with `VerifyEmailActionToken`).

### Authenticator implementation

With action token completed we may continue implementing `EmailLinkAuthenticator`. We will use `VerifyEmail.sendVerifyEmailEvent` as reference but once again since context is very different we will change a lot of things.

```java
    private static final Logger logger = Logger.getLogger(EmailLinkAuthenticator.class);

	protected void sendVerifyEmail(KeycloakSession session, AuthenticationFlowContext context, UserModel user)
            throws UriBuilderException, IllegalArgumentException {
        AuthenticationSessionModel authSession = context.getAuthenticationSession();
        RealmModel realm = session.getContext().getRealm();

        // use the same lifespan as other tokens by getting from realm configuration
        int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(EmailLinkActionToken.TOKEN_TYPE);
        long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);

        String link = buildEmailLink(session, context, user, validityInSecs);

        // event is used to achieve better observability over what happens in Keycloak
        EventBuilder event = getSendVerifyEmailEvent(context, user);

        Map<String, Object> attributes = getMessageAttributes(user, realm.getDisplayName(), link, expirationInMinutes);

        try {
            session.getProvider(EmailTemplateProvider.class)
                    .setRealm(realm)
                    .setUser(user)
                    .setAuthenticationSession(authSession)
                    // hard-code some of the variables - we will return here later
                    .send("emailLinkSubject", "email-link-email.ftl", attributes);

            event.success();
        } catch (EmailException e) {
            logger.error("Failed to send verification email", e);
            event.error(Errors.EMAIL_SEND_FAILED);
        }

        showEmailSentPage(context, user);
    }

    /**
     * Generates an action token link by encoding `EmailLinkActionToken` with user
     * and session data
     */
    protected String buildEmailLink(KeycloakSession session, AuthenticationFlowContext context, UserModel user,
            int validityInSecs) {
        AuthenticationSessionModel authSession = context.getAuthenticationSession();
        RealmModel realm = session.getContext().getRealm();
        UriInfo uriInfo = session.getContext().getUri();

        int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;

        String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
        EmailLinkActionToken token = new EmailLinkActionToken(user.getId(), absoluteExpirationInSecs,
                authSessionEncodedId, user.getEmail(), authSession.getClient().getClientId());
        UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
                authSession.getClient().getClientId(), authSession.getTabId());
        String link = builder.build(realm.getName()).toString();

        return link;
    }

    /**
     * Creates a Map with context required to render email message
     */
    protected Map<String, Object> getMessageAttributes(UserModel user, String realmName, String link,
            long expirationInMinutes) {
        Map<String, Object> attributes = new HashMap<>();

        attributes.put("user", user);
        attributes.put("realmName", realmName);
        attributes.put("link", link);
        attributes.put("expirationInMinutes", expirationInMinutes);

        return attributes;
    }

    /**
     * Creates a builder for `SEND_VERIFY_EMAIL` event
     */
    protected EventBuilder getSendVerifyEmailEvent(AuthenticationFlowContext context, UserModel user) {
        AuthenticationSessionModel authSession = context.getAuthenticationSession();

        EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL)
                .user(user)
                .detail(Details.USERNAME, user.getUsername())
                .detail(Details.EMAIL, user.getEmail())
                .detail(Details.CODE_ID, authSession.getParentSession().getId())
                .removeDetail(Details.AUTH_METHOD)
                .removeDetail(Details.AUTH_TYPE);

        return event;
    }

    /**
     * Displays email link form
     */
    protected void showEmailSentPage(AuthenticationFlowContext context, UserModel user) {
        String accessCode = context.generateAccessCode();
        URI action = context.getActionUrl(accessCode);

        Response challenge = context.form()
                .setStatus(Response.Status.OK)
                .setActionUri(action)
                .setExecution(context.getExecution().getId())
                .createForm("email-link-form.ftl");

        context.forceChallenge(challenge);
    }
```

This implementation does a few things:

1. Creates an event that will allow you to collect data on usage and debug if needed
2. Generates a link to an action token
3. Sends an email with a few attributes including the user and the link
4. Shows a form that allows you to resend the email or confirm that you verified your authentication via link

I took the liberty to hard-code a few variables temporarily - we will return to replace them with actual constants or configruation parameters. This includes `"emailLinkSubject"` being the message id for email subject and email and form templates: `"email-link-email.ftl"`, `"email-link-form.ftl"`. Messages and templates are stored in `resources` directory in Keycloak. We will put them there later.

Now let's implement the most important methods: `action` and `authenticate`.

```java
    @Override
    public void authenticate(AuthenticationFlowContext context) {
        // the method gets called when first reaching this authenticator and after page
        // refreshes
        AuthenticationSessionModel authSession = context.getAuthenticationSession();
        KeycloakSession session = context.getSession();
        RealmModel realm = context.getRealm();
        UserModel user = context.getUser();

        // cant really do anything without smtp server
        if (realm.getSmtpConfig().isEmpty()) {
            ServicesLogger.LOGGER.smtpNotConfigured();
            context.attempted();
            return;
        }

        // if email was verified allow the user to continue
        if (Objects.equals(authSession.getAuthNote(EMAIL_LINK_VERIFIED), user.getEmail())) {
            context.success();
            return;
        }

        // do not allow resending e-mail by simple page refresh
        if (!Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), user.getEmail())) {
            authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, user.getEmail());
            sendVerifyEmail(session, context, user);
        } else {
            showEmailSentPage(context, user);
        }
    }

    @Override
    public void action(AuthenticationFlowContext context) {
        // this method gets called when user submits the form
        AuthenticationSessionModel authSession = context.getAuthenticationSession();
        KeycloakSession session = context.getSession();
        UserModel user = context.getUser();

        // if link was already open continue authentication
        if (Objects.equals(authSession.getAuthNote(EMAIL_LINK_VERIFIED), user.getEmail())) {
            context.success();
            return;
        }

        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
        String action = formData.getFirst("submitAction");

        // if the form was submitted with an action of `resend` resend the email
        // otherwise just show the same page
        if (action != null && action.equals("resend")) {
            sendVerifyEmail(session, context, user);
        } else {
            showEmailSentPage(context, user);
        }
    }
```

The last thing we want to do with our authenticator class is create a factory for it. Keycloak uses factories to instantiate providers. For our action token handler factory was implemented in the base class. So it is entirely possible to implement both the provider and the factory in the same class. In this case we will implement it in a different class `EmailLinkAuthenticatorFactory`.

```java
package dev.yakovlev_alexey.keycloak.authentication.authenticators.browser;

import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.List;

public class EmailLinkAuthenticatorFactory implements AuthenticatorFactory {
	public static final EmailLinkAuthenticator SINGLETON = new EmailLinkAuthenticator();

	@Override
	public String getId() {
		return "email-link-authenticator";
	}

	@Override
	public String getDisplayType() {
		return "Email Link Authentication";
	}

	@Override
	public String getHelpText() {
		return "Authenticates the user with a link sent to their email";
	}

	@Override
	public String getReferenceCategory() {
		return null;
	}

	@Override
	public boolean isConfigurable() {
		return false;
	}

	@Override
	public boolean isUserSetupAllowed() {
		return false;
	}

	@Override
	public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
		return new AuthenticationExecutionModel.Requirement[] {
				AuthenticationExecutionModel.Requirement.REQUIRED,
				AuthenticationExecutionModel.Requirement.DISABLED,
		};
	}

	@Override
	public List<ProviderConfigProperty> getConfigProperties() {
		return null;
	}

	@Override
	public void init(Config.Scope config) {
	}

	@Override
	public void postInit(KeycloakSessionFactory factory) {
	}

	@Override
	public void close() {
	}

	@Override
	public Authenticator create(KeycloakSession session) {
		// a common pattern in Keycloak codebase is to use singletons for factories
		return SINGLETON;
	}
}
```

> At the moment we do not allow any configuration for our authenticator. Also our authenticator is either required or disabled. This means that it is not possible to bypass this authenticator by not having an email. However if this is what you want you may make this authenticator optional. You would also need to call `context.attempted()` in `authenticate` method in the authenticator class.

> Another option to allow emailless users to skip this authenticator is to create another conditional authenticator - however this is out of scope for this tutorial.

### Custom resources for plugins

We now have all the code we need for desired functionality to work. However we still miss the form and messages to display. To add resources to our plugins let's create `resources` directory in `src/main`. In it create a subdirectory `theme-resources`. It is used by Kecyloak to import templates and messages.

Messages should be stored in `messages` folder in a `.properties` file named like `messages_{language}.properties`.

Templates similarly are stored in `templates` folder. Email message templates are split into `html` and `text` subfolders. Some email clients may not render HTML - text version will be used then. Form templates should be stored at the root of `templates`.

> Other resources are stored similarly stored in `css`, `js`, `img` directories. Read more about in [official docs](https://www.keycloak.org/docs/latest/server_development/#adding-a-script-to-a-theme).

```
src
└── main
	├── java
	└── resources
		└── theme-resources
			├── messages
			│   └── messages_en.properties
			└── templates
				├── html
				│	└── email-link-email.ftl
				├── text
				│	└── email-link-email.ftl
				└── email-link-form.ftl
```

Keycloak uses [FreeMaker](https://freemarker.apache.org) to store and render templates. Read more about how Keycloak manages its themes in the [official documentation](https://www.keycloak.org/docs/latest/server_development/#_themes).

To implement the form in `email-link-form.ftl` for our plugin we will use the following template:

```xml
<#import "template.ftl" as layout>
<@layout.registrationLayout; section>
    <#if section = "header">
        ${msg("emailLinkTitle")}
    <#elseif section = "form">
        <form id="kc-register-form" action="${url.loginAction}" method="post">
            <div class="${properties.kcFormGroupClass!}">
                <button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="submitAction" id="confirm" value="confirm">${msg("emailLinkCheck")}</button>
                <button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="submitAction" id="resend" value="resend">${msg("emailLinkResend")}</button>
            </div>
        </form>
    </#if>
</@layout.registrationLayout>
```

> `msg` function allows you to place localized strings from `resources/messages`. They will be substituted by Keycloak during rendering. Otherwise the syntax is pure FreeMaker.

You can see our submit buttons have a name and a value. When you submit an HTML form via a button with a value this value is appended to form data sent to the server. This is what we were using in `action` handler to make sure we only resend email when user actually asks for it by clicking the button.

As for emails templates look a little different. HTML version in `email-link-email.ftl` look like this:

```xml
<html>
<body>
${kcSanitize(msg("emailLinkEmailBodyHtml",link, expirationInMinutes, realmName))?no_esc}
</body>
</html>
```

> Here we just use a message for all contents of our email. We also pass variables that would be substitued into the message, more on that later. `no_esc` allows us to render HTML as HTML and not as text. And `kcSanitize` is needed to ensure no dangerous markup ends up in the resulting document.

The text version of the same email look like this:

```
<#ftl output_format="plainText">
${msg("emailLinkEmailBody",link, expirationInMinutes, realmName)}
```

> Here we explicitely tell that we want a plain text document and not an HTML file. Otherwise it is basically the same except we do not want HTML here therefore do not call neither `kcSanitize` nor `no_esc`.

At this point we have all the templates except we do not have any of the messages our templates (and Providers) use. Our `messages_en.properties` file should look something like this:

```
emailLinkSubject=Your authentication link

emailLinkResend=Resend email
emailLinkCheck=I followed the link

emailLinkEmailBody=Your authentication link is {0}.\nIt will be valid for {1} minutes. Follow it to authenticate in {2} and then return to the original browser tab.
emailLinkEmailBodyHtml=<a href="{0}">Click here to authenticate</a>. The link will be valid for {1} minutes. Follow it to authenticate in {2} and then return to the original browser tab.
```

> You can see its just a file containing all the messages we need. If you specify any of the messages that already exist in Keycloak, they will not get replaced. To replace existing messages use [themes](https://www.keycloak.org/docs/latest/server_development/#_themes). Some messages may contain HTML markup. But as I already said they need to be properly sanitized and unescape to be used.

### Notify Keycloak of new Providers

At this point we have the code and the resources for our plugin. However if we build it, put into Keycloak plugins folder and run Keycloak nothing will happen. Keycloak will not know what to do with our code. To tell Keycloak that we want it to inject certain classes we have to add certain meta information to our built `jar` file. For this use `resources/META-INF` directory.

```
src
└── main
	├── java
	└── resources
		├── theme-resources
		└── META-INF
			└── services
			 	├── org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
			 	└── org.keycloak.authentication.AuthenticatorFactory
```

> Here we created 2 files in `services` subdirectory. Each file in this directory should be named after a base interface or class that our own classes implement or extend.

In each file we need to specify all classes of our own that derivate from the filename class on separate lines.

```bash
# org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
dev.yakovlev_alexey.keycloak.authentication.actiontoken.emaillink.EmailLinkActionTokenHandler

# org.keycloak.authentication.AuthenticatorFactory
dev.yakovlev_alexey.keycloak.authentication.authenticators.browser.EmailLinkAuthenticatorFactory
```

Finally we can build our project by running `mvn clean package`. A file with the name of `keycloak-email-link-auth-plugin-1.0-SNAPSHOT.jar` should be created in `target` folder. Copy this file to `docker/plugins` directory.

## Configure test bench

We are almost ready to test our plugin. However in order to properly do this we would need an SMTP server since our plugin sends emails. You could use some real SMTP server. But that would likely cost money and you will have to use a real email. It is much easier to use a mail trap service like [MailHog](https://github.com/mailhog/MailHog).

Add it as service to `docker-compose.yaml`

```yaml
version: "3.2"

services:
    keycloak:
        build:
            context: ./
            dockerfile: Dockerfile
            args:
                - KEYCLOAK_IMAGE=${KEYCLOAK_IMAGE}
        environment:
            KEYCLOAK_ADMIN: admin
            KEYCLOAK_ADMIN_PASSWORD: admin
            DB_VENDOR: h2
        volumes:
            - ./h2:/opt/keycloak/data/h2
        ports:
            - "8024:8080"

    mailhog:
        image: mailhog/mailhog
        ports:
            - 1025:1025
            - 8025:8025
```

> Port 1025 is used for SMTP and 8025 for web UI/API.

Now you can run Docker using `docker-compose up --build keycloak`. Visit http://localhost:8024 and enter administration console. Username and password should both be `admin` as stated in the yaml.

To use our authenticator you need to configure an authenticator flow that uses it. Flows are sequences of authenticators. By default `browser` authentication flow is used. Go ahead and copy it as `browser-email` in `Authentication` tab.

Replace `Username Password Form` with `Username Form`. Since we want to authenticate based on the link sent to the email we do not really want password from our users. Though it is also possible to keep both password and emails to implement a sort of 2 factor authentication.

After `Username Form` add our `Email Link Authentication` and make it `Required`.

![Flows screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/voy183vo7av34pk5zxhd.png)

Next configure SMTP server in `Realm Settings` -> `Email`. Specify any `From` value. Host for the connection should be `mailhog` and port `1025`. No SSL or authentication is used.

![SMTP configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/72izynwhzmh9nhy2n09w.png)

After you saved Email configuration run `Test Connection`. Email should be sent successfully and you should see a test message in MailHog web UI at http://localhost:8025/.

![MailHog UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xhyocoexahfdykfu16mj.png)

Make sure your admin user has a valid email (not necessarily one that you have access to) in `Users` tab.

Finally enter `Clients`, find `account-console` and in `Advanced` tab configure `Authentication flow overrides`. Browser flow should be overriden with `browser-email`. This will allow you to easily test your plugin by entering http://localhost:8024/realms/master/account/ and not break the admin panel if your changes do not work.

![Authentication flow overrides in client](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gyxeo5sgzi2o4qsnaojz.png)

Everything is set up and you can now visit http://localhost:8024/realms/master/account/ and enter `admin` as your username. You should see the form we implemented earlier and an email should be visible in MailHog UI at http://localhost:8025/.

![Email authentication form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qd5ewapq2tcapoweyum8.png)

Visit the link from the message.

![Message in MailHog UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/he44aqi9uj6v8gm9vn8e.png)

You should see the link has been verified.

![Verified link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hboy8xc74tt0btymedtv.png)

Return to the original tab and voila! You should be authenticated now.

![Authenticated in account console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ldxc1nwalrwa9x6e1ayw.png)

## Authenticator configuration

You may remember that we hardcoded some of the variables that should either be configurable or at least stored separately. Let's go back and fix that.

Create a folder to store all utility classes for our authenticator.

```
src
└── main
	└── java
		└── dev
			└── yakovlev_alexey
				└── keycloak
					└── authentication
					│	├── actiontoken
					│	└── authenticators
					└── emaillink
						├── ConfigurationProperties.java
						├── Constants.java
						└── Messages.java
```

> `ConfigurationProperties.java` is a class with constants to enable configuration for our authenticator. `Constants.java` stores generic variable that do not need to be configurable. And `Messages.java` contains all message string keys our plugin needs. Some developers put there only the keys that Java code needs but I prefer to use `Messages` class as a list for all strings (even if they are only used in templates) so that it is easy to add localized versions (e.g. `messages_de.properties` file).

Let's implement `ConfigurationProperties` class:

```java
package dev.yakovlev_alexey.keycloak.emaillink;

import org.keycloak.provider.ProviderConfigProperty;

import java.util.Arrays;
import java.util.List;

import static org.keycloak.provider.ProviderConfigProperty.*;

public final class ConfigurationProperties {
	public static final String EMAIL_TEMPLATE = "EMAIL_TEMPLATE";
	public static final String PAGE_TEMPLATE = "PAGE_TEMPLATE";

	public static final String RESEND_ACTION = "RESEND_ACTION";

	public static final List<ProviderConfigProperty> PROPERTIES = Arrays.asList(
			new ProviderConfigProperty(EMAIL_TEMPLATE,
					"FTL email template name",
					"Will be used as the template for emails with the link",
					STRING_TYPE, "email-link-email.ftl"),
			new ProviderConfigProperty(PAGE_TEMPLATE,
					"FTL page template name",
					"Will be used as the template for email link page",
					STRING_TYPE, "email-link-form.ftl"),
			new ProviderConfigProperty(RESEND_ACTION,
					"Resend Email Link action",
					"Action which corresponds to user manually asking to resend email with link",
					STRING_TYPE, "resend"));

	private ConfigurationProperties() {
	}
}
```

And `Constants` class:

```java
package dev.yakovlev_alexey.keycloak.emaillink;

public final class Constants {
	public static final String EMAIL_LINK_SUBMIT_ACTION_KEY = "submitAction";

	private Constants() {
	}
}
```

And finally `Messages` class:

```java
package dev.yakovlev_alexey.keycloak.emaillink;

public final class Messages {
	public static final String EMAIL_LINK_SUBJECT = "emailLinkSubject";
	public static final String EMAIL_LINK_STALE = "emailLinkStale";

	public static final String EMAIL_LINK_SUCCESS = "emailLinkSuccess";

	public static final String EMAIL_LINK_TITLE = "emailLinkTitle";
	public static final String EMAIL_LINK_RESEND = "emailLinkResend";
	public static final String EMAIL_LINK_CHECK = "emailLinkCheck";

	public static final String EMAIL_LINK_EMAIL_BODY = "emailLinkEmailBody";
	public static final String EMAIL_LINK_EMAIL_BODY_HTML = "emailLinkEmailBodyHtml";

	private Messages() {
	}
}
```

A few changes in `EmailLinkAuthenticatorFactory` are needed to make it configurable:

```java
	@Override
	public boolean isConfigurable() {
		return true;
	}

	@Override
	public List<ProviderConfigProperty> getConfigProperties() {
		return ConfigurationProperties.PROPERTIES;
	}
```

Now update `EmailLinkAuthenticator` class to leverage new constants:

```java
	// ***

    @Override
    public void action(AuthenticationFlowContext context) {
        // this method gets called when user submits the form
        AuthenticationSessionModel authSession = context.getAuthenticationSession();
        AuthenticatorConfigModel config = context.getAuthenticatorConfig();
        KeycloakSession session = context.getSession();
        UserModel user = context.getUser();

        // if link was already open continue authentication
        if (Objects.equals(authSession.getAuthNote(EMAIL_LINK_VERIFIED), user.getEmail())) {
            context.success();
            return;
        }

        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
        String action = formData
                .getFirst(dev.yakovlev_alexey.keycloak.emaillink.Constants.EMAIL_LINK_SUBMIT_ACTION_KEY);

        // if the form was submitted with an action of `resend` resend the email
        // otherwise just show the same page
        if (action != null && action.equals(config.getConfig().get(ConfigurationProperties.RESEND_ACTION))) {
            sendVerifyEmail(session, context, user);
        } else {
            showEmailSentPage(context, user);
        }
    }

	// ***

	private void sendVerifyEmail(KeycloakSession session, AuthenticationFlowContext context, UserModel user)
            throws UriBuilderException, IllegalArgumentException {
        AuthenticationSessionModel authSession = context.getAuthenticationSession();
        AuthenticatorConfigModel config = context.getAuthenticatorConfig();
        RealmModel realm = session.getContext().getRealm();

        // use the same lifespan as other tokens by getting from realm configuration
        int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(EmailLinkActionToken.TOKEN_TYPE);
        long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);

        String link = buildEmailLink(session, context, user, validityInSecs);

        // event is used to achieve better observability over what happens in Keycloak
        EventBuilder event = getSendVerifyEmailEvent(context, user);

        Map<String, Object> attributes = getMessageAttributes(user, realm.getDisplayName(), link, expirationInMinutes);

        try {
            session.getProvider(EmailTemplateProvider.class)
                    .setRealm(realm)
                    .setUser(user)
                    .setAuthenticationSession(authSession)
                    // hard-code some of the variables - we will return here later
                    .send(config.getConfig().get(Messages.EMAIL_LINK_SUBJECT),
                            config.getConfig().get((ConfigurationProperties.EMAIL_TEMPLATE)), attributes);

            event.success();
        } catch (EmailException e) {
            logger.error("Failed to send verification email", e);
            event.error(Errors.EMAIL_SEND_FAILED);
        }

        showEmailSentPage(context, user);
    }


	// ***

    /**
     * Displays email link form
     */
    protected void showEmailSentPage(AuthenticationFlowContext context, UserModel user) {
        AuthenticatorConfigModel config = context.getAuthenticatorConfig();
        String accessCode = context.generateAccessCode();
        URI action = context.getActionUrl(accessCode);

        Response challenge = context.form()
                .setStatus(Response.Status.OK)
                .setActionUri(action)
                .setExecution(context.getExecution().getId())
                .createForm(config.getConfig().get(ConfigurationProperties.PAGE_TEMPLATE));

        context.forceChallenge(challenge);
    }
```

> You can access authenticator configuration via `context.getAuthenticatorConfig()`. `Constants` overlaps with Keycloak `Constants` class used in the same file therefore to avoid name clashes full package name is specified here.

Now when you enter `Authentication` tab and edit `browser-email` flow you should see a gear button beside `Email Likn Authentication` that will open settings for this authenticator.

> In Keycloak 19 there is a bug in interface that does not allow opening settings for authenticators. Refer to [this issue](https://github.com/keycloak/keycloak-ui/issues/3375).

## Next steps

To make sure you get a good grasp of how to develop Keycloak plugins I recommend you do a few "homework" tasks yourself. There are some things that could be improved about this plugin that I already mentioned.

1. Implement a configuration option that would enable a confirmation screen when you open the link. This screen should have a "confirm" button. Only after clicking this button you may continue in your original tab. If link is open in the same browser window user should be immideately authenticated as it works now.

2. Create a separate plugin that implements a condition authenticator `HasEmailCondition`. You may use [`ConditionalUserAttributeValue`](https://github.com/keycloak/keycloak/blob/43a3677cc727469a7c0d46c95436420e0ff3feb5/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalUserAttributeValue.java) as reference. Use both plugins in a test bench to configure a flow where users with password set up are authenticated via password and others are required to follow email link.

## Conclusion

I hope this tutorial gave you an idea how to create Keycloak plugins. There is much more to it, many internal SPIs could be implemented to achieve different scenarios. My goal was to focus on the basics: plugin composition, resources imports and getting inspiration from Keycloak source code. If you have any questions, ideas or suggestions make sure to leave them in [GitHub issues](https://github.com/yakovlev-alexey/keycloak-email-link-auth/issues/new).

Find the complete code on my GitHub - https://github.com/yakovlev-alexey/keycloak-email-link-auth.