mesutpiskin / keycloak-2fa-email-authenticator

🔒 Keycloak Authentication Provider implementation to get a two factor authentication with a OTP/code/token send via Email (through SMTP)
https://medium.com/@mesutpiskin/two-factor-authentication-via-email-in-keycloak-custom-auth-spi-935bbb3952a8
Apache License 2.0
158 stars 89 forks source link

OTP Email Theming is Broken #30

Open dcarlet opened 10 months ago

dcarlet commented 10 months ago

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:

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:

However, testing revealed a consistent behavior:

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 user=david.carlet@arkloud.onmicrosoft.us: 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.

kominoshja commented 2 weeks ago

This gets fixed by https://github.com/mesutpiskin/keycloak-2fa-email-authenticator/pull/21. A rebase of that branch is needed, but from my tests it still works fine.

kn3609571 commented 5 days ago

This issue may be solved easier by including the templates as theme-resources in the JAR. https://www.keycloak.org/docs/latest/server_development/#_theme_resource