6

Our backend is currently using the KeyCloak Admin Client API (Java) to

  • Create users
  • Create roles
  • Assign roles to users
  • Executing actions emails (“UPDATE_PASSWORD”, “UPDATE_PROFILE”, “VERIFY_EMAIL”)

Our flow however needs to support the following scenario :

  • Instead of using the executeActionsEmail API call and have Keycloak send out an email to users for them to complete their profile we would like to use an external email service / template to send out these mails
  • When Keycloak sends out an UPDATE_PROFILE email it contains a /login-actions/action-token?key=eyJhbG… link with an action-token.
  • We would like to embed this link into our own email templates (outside of keycloak)

So the questions is can I use the KeyCloak API (or some other mechanism) to generate login actions URLs for a user inside a keycloak realm that we can then use in an external email template to send out a complete registration email ?

Danny Paul
  • 415
  • 1
  • 6
  • 8
ddewaele
  • 22,363
  • 10
  • 69
  • 82

1 Answers1

1

You can implement custom service providers by implementing a particular service provider interface. For your case, you could implement a custom resource provider that returns an action token based on some input parameters.

Implementation instructions for a custom REST API endpoint are provided in the documentation. You can also check this article, that pretty much covers your scenario.

I played around with it a bit, here's an example:

public class ExecuteActionsTokenResourceProvider implements RealmResourceProvider {

    private static final Logger log = Logger.getLogger(ExecuteActionsTokenResourceProvider.class);

    private final KeycloakSession session;

    public ExecuteActionsTokenResourceProvider(KeycloakSession session) {
        this.session = session;
    }

    @POST
    @Path("action-tokens")
    @Produces({MediaType.APPLICATION_JSON})
    public Response getActionToken(
        @QueryParam("userId") String userId,
        @QueryParam("email") String email,
        @QueryParam("redirectUri") String redirectUri,
        @QueryParam("clientId") String clientId,
        @Context UriInfo uriInfo) {

        KeycloakContext context = session.getContext();
        RealmModel realm = context.getRealm();
        int validityInSecs = realm.getActionTokenGeneratedByUserLifespan();
        int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;

        ClientModel client = assertValidClient(clientId, realm);

        assertValidRedirectUri(redirectUri, client);

        // Can parameterize this as well
        List requiredActions = new LinkedList();
        requiredActions.add(RequiredAction.UPDATE_PASSWORD.name());

        String token = new ExecuteActionsActionToken(
            userId,
            absoluteExpirationInSecs,
            requiredActions,
            redirectUri,
            clientId
        ).serialize(
            session,
            context.getRealm(),
            uriInfo
        );

        return Response.status(200).entity(token).build();
    }

    private void assertValidRedirectUri(String redirectUri, ClientModel client) {
        String redirect = RedirectUtils.verifyRedirectUri(session, redirectUri, client);
        if (redirect == null) {
            throw new WebApplicationException(
                ErrorResponse.error("Invalid redirect uri.", Status.BAD_REQUEST));
        }
    }

    private ClientModel assertValidClient(String clientId, RealmModel realm) {
        ClientModel client = realm.getClientByClientId(clientId);
        if (client == null) {
            log.debugf("Client %s doesn't exist", clientId);
            throw new WebApplicationException(
                ErrorResponse.error("Client doesn't exist", Status.BAD_REQUEST));
        }
        if (!client.isEnabled()) {
            log.debugf("Client %s is not enabled", clientId);
            throw new WebApplicationException(
                    ErrorResponse.error("Client is not enabled", Status.BAD_REQUEST));
        }
        return client;
    }

    @Override
    public Object getResource() {
        return this;
    }

    @Override
    public void close() {
        // Nothing to close.
    }
}

You would then package this class in a JAR and deploy it to Keycloak, like described in the deployment documentation. There are also lots of examples of this on Github.

David Fenko
  • 131
  • 2
  • 8
  • I tried to replicate this in my local keyclaok, but when tried to access the API from postman, its returning 404 Not Found error. I have posted this here Can you please check this?... https://stackoverflow.com/questions/76869266/custom-api-in-keycloak-is-returning-http-404-not-found – Kavi Chinna Aug 10 '23 at 05:11