How to bind client role + group to represent a unique "role" ? (app level multi-tenancy)

Hello,
So here is my situation :
The applications using keycloak as their authentication server need to be able to grant access to features based on the couple “role+company”.
For instance : I log into my app, select somehow I’m working for “company A” and the app can see I am granted “admin” role specifically for “company A”.
If I log for “company B”, maybe I’m just “contributor” and so don’t have access to the same set of features.

Here is what I did :

  • Companies are stored as keycloak “groups” (ex : “company-A”, “company-B”)
  • Apps are “clients” (ex : “some-awesome-app”)
  • App roles are “client roles” (ex : “admin”, “contributor”, “guest”)

So in the (happy) simple case (which we also need), when no specific company is needed, it works seamlessly, and the app get an access token with plain an simple "group: ‘company X’, ‘resource_access’:‘some-app’:‘admin’).

Things get tricky when the app needs to use the “role+company” feature.
In this case, I create another client role named for example “admin@company-a”, so the information “this user has admin access for company-a” is conveyed through the access token in the “resource_access” section (the app has to split on the ‘@’ to decode the information).

Is it a good approach ?
Can keycloak handle a large number of client roles ? (each role can be duplicated for each group)
Is there a best way to handle that feature ?

NB : group membership has not the same meaning as “role@group”. It is absolutely possible that the user is a member of “company-a” but use an app on behalf of “company-b” and then needs the “role@company-b” info…

NB2 : we were very interested in the “organizations” feature but it does not seem to fit our use-case. But maybe I didn’t get it ? :slight_smile:

Thanks

I finally found this which describes the exact same need as mine :

Seems not resolved though :frowning:

Hi Vincent,

Yes we could not really find a way to do this nicely out of the box. Currently we are abusing the group system as I mentioned in the initial post there.

It sort of works for our use case as we have the claim in the token

"new-claim": [
  "/tenantA/role1"
  "/tenantA/role2",
  "/tenantB/role2",
  "/tenantC/role1",
  "/tenantC/role3"
]

Which we can then parse in our backend to achieve what we want, but it is not ideal, especially as we have duplications of the roles as the subgroups need to be created for each tenant/company.

Keep me posted if you find out a nicer approach,
Jonas

Hi !
Thanks a lot for your input !

Ok so you’re abusing the group system while I’m abusing de role system :smiley:

Indeed I discarded the group approach because of all of the duplication.
We have currently ~25000 “tenants” and growing, so I don’t feel it would end up nicely… although it could be very true with client roles either…

Have you run into performance problem yet ?

We are a bit under 25000, and not too far along the intergration as we are rolling this out with a new app, so no performance issues yet, but I see why you would not want to!

Another approach we considered was to introduce a multivalued user attribute which would contain values like tenant:role. You could set it to only visible/editable by admins and then make a protocol mapper to add it to the tokens similar to the group claim, maybe that scales better than the groups for your case.

Cheers, Jonas

Oh that’s an interesting approach. I’ll look into that.

Thanks a lot !

Hello,

To improve performance in a multi-tenant setup, you can retrieve the JWT token by scope, allowing the app to authenticate based on the tenant and group context. For example, if you’re authenticated in the app for company-A, the token could look like this:

Example 1: Token for company-A with groupG

This token represents a user authenticated in company-A and assigned the roles for groupG within that company.

{
  "sub": "<snip>",
  "name": "Vincent",
  "scopes": "openid tenant:company-a group:groupG",
  "current_tenant": "company-a",
  "current_group": "groupG",
  "tenants": {
    "company-a": {
      "groups": {
        "groupG": ["role1", "role2"]
      },
      "tenantRoles": ["admin", "contributor"]
    }
  },
  "iss": "https://kc.app.com/"
}

Example 2: Token for company-B with groupA

This token represents the same user but authenticated in company-B and assigned roles for groupA within that company.

{
  "sub": "<snip>",
  "name": "Vincent",
  "scopes": "openid tenant:company-b group:groupA",
  "current_tenant": "company-b",
  "current_group": "groupA",
  "tenants": {
    "company-b": {
      "groups": {
        "groupA": ["role1", "role2"]
      },
      "tenantRoles": ["guest"]
    }
  },
  "iss": "https://kc.app.com/"
}

Explanation:

  1. Example 1 shows the token for company-A, where the user is in groupG and has role1 and role2 assigned.
  2. Example 2 shows the token for company-B, where the same user is now in groupA and has role1 and role2 in that context, but also has different tenantRoles for that company.

By using the scopes tenant:company-a or tenant:company-b, the app can retrieve the correct roles and permissions for the user’s specific group and company context. This approach helps improve performance by retrieving only the necessary data for the authenticated tenant.

Hello @Aym, thanks a lot for your input.

If I am not mistaken, it means a scope is created for each tenant, right ?
Is it OK for keycloak to handle 25000+ scopes ?

Or do you suggest using the “dynamic scopes” experimental feature ? Seems really interesting but it has been flagged as “experimental” for a long time now… So it probably does not fit for production use…