anarsultanov / keycloak-multi-tenancy

Keycloak extension for creating multi-tenant IAM for B2B SaaS applications.
Apache License 2.0
103 stars 11 forks source link

Multi-Tenancy Unknown error on enabling Tenant creation in Authentication #1

Closed mamunmohamed closed 10 months ago

mamunmohamed commented 10 months ago

When attempting to enable the creation of tenants in the authentication section of my Keycloak instance, I'm encountering an unknown error. This error prevents me from successfully configuring multi-tenancy for my application.

Steps followed to enable Create tenant:

  1. Keycloak Admin Console
  2. Navigate to the authentication section.
  3. Enable th option for creating tenant
  4. Show unknown error message when tried log with user assigned.

image

image

I expect to be able to enable the creation of tenants without encountering any errors. This would allow me to configure multi-tenancy for my application as needed. However, an unknown error is displayed when attempting to enable the creation of tenants. This prevents me from proceeding with the multi-tenancy configuration.

I am using keycloak version 22.0.1

I would greatly appreciate any assistance in resolving this issue. If there's any further information required from my side, please let me know.

Thank you for your help!

anarsultanov commented 10 months ago

Hello @mamunmohamed

Thank you for reporting the issue you're facing, and I appreciate the details you've provided.

Unfortunately, I couldn't replicate the error you mentioned. Generally, after enabling tenant creation, you should receive a response like:

{
    "error": "invalid_grant",
    "error_description": "Account is not fully set up"
}

If you're able to provide the logs of your Keycloak instance when this error occurs, it would greatly assist in resolving the issue. In case you're unable to find the specific logs related to this issue, you might consider enabling debug-level logging.

Fictor86 commented 10 months ago

Hello, good morning! I'm a colleague of Mamun. The .JAR is compiled and placed inside the providers folder with the corresponding /bin/kc.bat build command.

We see everything related to the extension in Authentication and Mappers. Without activating anything, the Postman call works correctly for one of the users. However, when we activate it, these are the error traces that we see in Intellij:

2023-08-28 08:39:30,827 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-10) Uncaught server error: java.lang.NoSuchMethodError: 'javax.persistence.EntityManager org.keycloak.connections.jpa.Jpa
ConnectionProvider.getEntityManager()'
        at dev.sultanov.keycloak.multitenancy.model.jpa.JpaTenantProviderFactory.create(JpaTenantProviderFactory.java:21)
        at dev.sultanov.keycloak.multitenancy.model.jpa.JpaTenantProviderFactory.create(JpaTenantProviderFactory.java:10)
        at org.keycloak.services.DefaultKeycloakSession.getProvider(DefaultKeycloakSession.java:177)
        at jdk.internal.reflect.GeneratedMethodAccessor8.invoke(Unknown Source)
        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.ContextParameterInjector$GenericDelegatingProxy.invoke(ContextParameterInjector.java:146)
        at jdk.proxy2/jdk.proxy2.$Proxy50.getProvider(Unknown Source)
        at dev.sultanov.keycloak.multitenancy.authentication.requiredactions.CreateTenant.evaluateTriggers(CreateTenant.java:26)
        at org.keycloak.services.managers.AuthenticationManager.evaluateRequiredAction(AuthenticationManager.java:1411)
        at org.keycloak.services.managers.AuthenticationManager.lambda$evaluateRequiredActionTriggers$22(AuthenticationManager.java:1382)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
        at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
        at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
        at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.base/java.util.stream.ReferencePipeline.forEachOrdered(ReferencePipeline.java:601)
        at org.keycloak.services.managers.AuthenticationManager.evaluateRequiredActionTriggers(AuthenticationManager.java:1382)
        at org.keycloak.services.managers.AuthenticationManager.nextRequiredAction(AuthenticationManager.java:1100)
        at org.keycloak.authentication.AuthenticationProcessor.nextRequiredAction(AuthenticationProcessor.java:1132)
        at org.keycloak.protocol.AuthorizationEndpointBase.handleBrowserAuthenticationRequest(AuthorizationEndpointBase.java:138)
        at org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.buildAuthorizationCodeAuthorizationResponse(AuthorizationEndpoint.java:356)
        at org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.process(AuthorizationEndpoint.java:226)
        at org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.processInRetriableTransaction(AuthorizationEndpoint.java:147)
        at org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint.buildGet(AuthorizationEndpoint.java:119)
        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:142)
        at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:168)
        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:833)

Thx in advance for all!

anarsultanov commented 10 months ago

Hi @Fictor86

Thank you for sharing the log.

It seems that the error is likely due to building the extension from an outdated branch. Keycloak 22 and its corresponding extension version use jakarta dependencies, while the log shows javax.persistence.EntityManager.

To resolve this, I suggest rebuilding the extension from the latest main branch or using a pre-built JAR from the releases.

Fictor86 commented 10 months ago

Thank you, that was exactly it. At the moment, we can already see the Tenant creation screens. We had a previous version and didn't realize that could be the issue. Appreciate the quick response and friendliness.

A pleasure!

anarsultanov commented 10 months ago

You're welcome! Glad the problem has been resolved. If you have any other issues or suggestions for improvement, please don't hesitate to reach out.

Fictor86 commented 10 months ago

Hello again!

Great, now that this has been resolved, we have created two Tenants for two users in a Realm. One user is an admin, and the other is a regular user. Later, using Postman, we added an additional Tenant to one of these users through the POST request -> http://localhost:8080/realms/multi-tenant/tenants.

This process works well, and during the login, a selection screen appears, allowing the user to choose which Tenant to access. However, our issue arises when we attempt to query a user who is part of two Tenants. When we inspect their token, we encounter the error you mentioned:

{
    "error": "invalid_grant",
    "error_description": "Account is not fully set up"
}

We are unable to delete one of the Tenants because we no longer have a valid token to access the DELETE functionality. What could be causing this issue? Is there some additional configuration we are missing?

anarsultanov commented 10 months ago

Hi @Fictor86

It seems like the issue you're facing is related to the "Select active tenant" required action that's enabled. This requirement might prevent you from obtaining a token for a multi-tenant user, leading to the "Account is not fully set up" error. To address this, you might need to disable the "Select active tenant" action temporarily, which could allow you to delete one of the tenants.

In addition, you should probably avoid adding tenant admins to multiple tenants. However, I understand that this solution might not be ideal for some use cases. Another option could be to allow including the tenant name as part of the token request.

I recommend creating a separate issue to address this matter further. Feel free to share any suggestions you have for improving the current setup based on your specific use case.

mamunmohamed commented 10 months ago

Hi @anarsultanov,

What we are trying to implement is:

1 User can be part of more than 1 tenant. At the time of login, the user can select to which tenant to Log. Here, we are facing the issue that you described at first paragraph.

image

### Another option could be to allow including the tenant name as part of the token request. ---> We would be very thankful to you if you can give us hint to implement it.

Kind regards

anarsultanov commented 10 months ago

Hi @mamunmohamed

User can be part of more than 1 tenant. At the time of login, the user can select to which tenant to Log.

That's the purpose of this extension, and it should work if you use the authorization code grant type. The problem you describe can only occur if you use the password grant type that is generally not recommended at all.

We would be very thankful to you if you can give us hint to implement it.

I understand that there may be use cases with high-trusted applications where this grant type may be required, so I asked you to create another issue about this as it should probably be implemented in the extension itself.

But I still recommend that you refrain from using the password grant type and consider the authorization code grant type, as it has been deprecated by the OAuth working group. The OAuth security best practices say you MUST NOT use the resource owner password credentials grant, and the grant is purposely omitted from OAuth 2.1.

Fictor86 commented 10 months ago

Thank you very much for everything, the speed and the service, and thank you for all the development work on this extension. We just wanted to demonstrate, with this message, our gratitude.