keycloak-2fa-email-authenticator icon indicating copy to clipboard operation
keycloak-2fa-email-authenticator copied to clipboard

OTP Email Theming is Broken

Open dcarlet opened this issue 6 months ago • 0 comments

So, we recently were asked to add Email One Time Passcodes to our Keycloak. I was very happy to find this repository!

However, after several hours of testing/compiling/deploying/pulling hairs out, I've found that there seems to be an issue with how the plugin itself handles theme lookup.

We deploy Keycloak into Kubernetes using the bitnami container/helm chart. We're running 22.0.5. In order to add this awesome plugin, I:

  • Clone this repository and built from master after updating the pom.xml to specify a new custom version (v0.4-KC22.0.5-custom) and updating the keycloak.version to 22.0.5 using openJDK 17 on RHEL 8.

  • a dockerfile to build a custom container:

FROM docker.io/bitnami/keycloak:22.0.5-debian-11-r4
# Copy over the plugin itself
COPY keycloak-2fa-email-authenticator-v0.4-KC22.0.5-custom.jar /opt/bitnami/keycloak/providers/
# # Copy over the template resources
COPY themes/ /opt/bitnami/keycloak/themes/
# Build it.
RUN /opt/bitnami/keycloak/bin/kc.sh build

ENTRYPOINT ["/opt/bitnami/scripts/keycloak/entrypoint.sh"]

CMD ["/opt/bitnami/scripts/keycloak/run.sh"]

I tried having the themes/ dir containing several things, and here are the results:

  1. Nothing
  2. The email code theme items per the README under /opt/bitnami/keycloak/themes/base/(etc)
  3. The email code theme as a folder under themes/ Did not work
  4. I additionally copied the themes/ out from the org.keycloak.keycloak-themes-22.0.5.jar jar so that /opt/bitnami/keycloak/themes/ looks like:
  • base
  • email-code-theme
  • keycloak
  • keycloak.v2

However, testing revealed a consistent behavior:

  • Email verification works
  • Email OTP does not work with the following stack trace:
2023-12-21 18:36:42,047 ERROR [com.mesutpiskin.keycloak.auth.email.EmailAuthenticatorForm] (executor-thread-2) Failed to send access code email. realm=d2dfd77d-5b84-484f-a89b-b7896349df06 [email protected]: org.keycloak.email.EmailException: Failed to template email
	at org.keycloak.email.freemarker.FreeMarkerEmailTemplateProvider.processTemplate(FreeMarkerEmailTemplateProvider.java:242)
	at org.keycloak.email.freemarker.FreeMarkerEmailTemplateProvider.send(FreeMarkerEmailTemplateProvider.java:257)
	at org.keycloak.email.freemarker.FreeMarkerEmailTemplateProvider.send(FreeMarkerEmailTemplateProvider.java:252)
	at com.mesutpiskin.keycloak.auth.email.EmailAuthenticatorForm.sendEmailWithCode(EmailAuthenticatorForm.java:179)
	at com.mesutpiskin.keycloak.auth.email.EmailAuthenticatorForm.generateAndSendEmailCode(EmailAuthenticatorForm.java:77)
	at com.mesutpiskin.keycloak.auth.email.EmailAuthenticatorForm.challenge(EmailAuthenticatorForm.java:44)
	at org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator.challenge(AbstractUsernameFormAuthenticator.java:66)
	at com.mesutpiskin.keycloak.auth.email.EmailAuthenticatorForm.authenticate(EmailAuthenticatorForm.java:39)
	at org.keycloak.authentication.DefaultAuthenticationFlow.processSingleFlowExecutionModel(DefaultAuthenticationFlow.java:445)
	at org.keycloak.authentication.DefaultAuthenticationFlow.processFlow(DefaultAuthenticationFlow.java:249)
	at org.keycloak.authentication.DefaultAuthenticationFlow.processSingleFlowExecutionModel(DefaultAuthenticationFlow.java:380)
	at org.keycloak.authentication.DefaultAuthenticationFlow.processFlow(DefaultAuthenticationFlow.java:249)
	at org.keycloak.authentication.DefaultAuthenticationFlow.processSingleFlowExecutionModel(DefaultAuthenticationFlow.java:380)
	at org.keycloak.authentication.DefaultAuthenticationFlow.processFlow(DefaultAuthenticationFlow.java:271)
	at org.keycloak.authentication.AuthenticationProcessor.authenticateOnly(AuthenticationProcessor.java:1026)
	at org.keycloak.services.resources.LoginActionsService$2.authenticateOnly(LoginActionsService.java:874)
	at org.keycloak.authentication.AuthenticationProcessor.authenticate(AuthenticationProcessor.java:888)
	at org.keycloak.services.resources.LoginActionsService.processFlow(LoginActionsService.java:380)
	at org.keycloak.services.resources.LoginActionsService.brokerLoginFlow(LoginActionsService.java:904)
	at org.keycloak.services.resources.LoginActionsService.postBrokerLoginGet(LoginActionsService.java:809)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:154)
	at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:118)
	at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:560)
	at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:452)
	at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:413)
	at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
	at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:415)
	at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:378)
	at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:174)
	at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:131)
	at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:33)
	at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:429)
	at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:240)
	at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:154)
	at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
	at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:157)
	at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:229)
	at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:82)
	at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:147)
	at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:84)
	at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:44)
	at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284)
	at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:177)
	at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
	at io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers$1.handle(HttpServerCommonHandlers.java:58)
	at io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers$1.handle(HttpServerCommonHandlers.java:36)
	at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284)
	at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:177)
	at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
	at org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter.lambda$createBlockingHandler$0(QuarkusRequestFilter.java:82)
	at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:576)
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
	at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
	at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: org.keycloak.email.EmailException: Failed to template plain text email.
	at org.keycloak.email.freemarker.FreeMarkerEmailTemplateProvider.processTemplate(FreeMarkerEmailTemplateProvider.java:230)
	... 60 more
Caused by: org.keycloak.theme.FreeMarkerException: Failed to process template text/code-email.ftl
	at org.keycloak.theme.freemarker.DefaultFreeMarkerProvider.processTemplate(DefaultFreeMarkerProvider.java:52)
	at org.keycloak.email.freemarker.FreeMarkerEmailTemplateProvider.processTemplate(FreeMarkerEmailTemplateProvider.java:228)
	... 60 more
Caused by: freemarker.template.TemplateNotFoundException: Template not found for name "text/code-email.ftl".
The name was interpreted by this TemplateLoader: org.keycloak.theme.freemarker.DefaultFreeMarkerProvider$ThemeTemplateLoader@6e291479.
	at freemarker.template.Configuration.getTemplate(Configuration.java:2957)
	at freemarker.template.Configuration.getTemplate(Configuration.java:2777)
	at org.keycloak.theme.freemarker.DefaultFreeMarkerProvider.getTemplate(DefaultFreeMarkerProvider.java:66)
	at org.keycloak.theme.freemarker.DefaultFreeMarkerProvider.processTemplate(DefaultFreeMarkerProvider.java:45)
	... 61 more

Part of what I discovered is that the plugin seems to be expecting to use the template based on what the Realm Settings -> Themes -> Email Code Theme setting is, set to. However, Keycloak uses this same setting for email verification. So if we change that setting to the email-code-theme, then Email OTP works, but keycloak's email verification does not. If we set it to Base or Keycloak, then Email verification works, but Email OTP does not.

I tried looking at the java source to figure out how to figure out how to set the plugin to use a different theme but I couldn't figure it out (I haven't been a Java dev in like, 10 years, so forgive me XD). It seems to be set on this line but...clearly something isn't correct.

dcarlet avatar Dec 21 '23 19:12 dcarlet