AF
Ash Framework•5mo ago
Vahagn

Multitenancy plug

Hi. I am trying to create a multitenant system eith AshAuthentication and AshGraphql. However, I could not get actor put in context, so the tenant too. In my case single actor can have access to multiple tenants. Let's assume there is a function get_selected_tenant(conn) and the User resource has_many Tenants (resource) with a role. I want to call PlugHelpers.put_tenant if the actor has any role in their selected tenant. Currently I could not even get the actor from conn (using :load_from_bearer plug, before my plug which will set the tenant)
33 Replies
Vahagn
VahagnOP•5mo ago
pipeline :graphql do
plug :load_from_bearer
plug :set_actor, :user
plug AldenteWeb.TenantCheckerPlug
plug AshGraphql.Plug
end
pipeline :graphql do
plug :load_from_bearer
plug :set_actor, :user
plug AldenteWeb.TenantCheckerPlug
plug AshGraphql.Plug
end
barnabasj
barnabasj•5mo ago
could you give a couple more details on how you are testing this and how you are trying to get the actor from the con in your plug
Vahagn
VahagnOP•5mo ago
Sorry, just tried different graphql playground and I believe the default (:simple) one did not send the header, Now it correctly picked up the actor Is there a way to store the permission info in JWT? I think it is not a good idea to query database on every request I mean I just want to load one relation before generating the token, and I want that relation to be saved in JWT
barnabasj
barnabasj•5mo ago
That's something @jart might be able to answer once the sun is rising in NZ
ZachDaniel
ZachDaniel•5mo ago
There is a lot of additional complications if you go that route you have to invalidate JWTs that contain incorrect information if something about a user's permissions change for example Are you sure you want to deal w/ that headache?
Vahagn
VahagnOP•5mo ago
I have already implemented that in TypeScript. and I was migrating my project to Ash. I understand that I will need to support older structures in JWT payload in case it changes. I need to invalidate the old ones in case user gains permission in a mew org or change in the existing I just think it would be more performant to have JWT contain the information every route and every action will need Also. the frontend will have a source of permissions and orgs in JWT (both client and server side) I though just preloading some relations to be included in the JWT will be a couple line change. I just don't know how to approach the problem to not change a big part of generated Ash Authentication code
ZachDaniel
ZachDaniel•5mo ago
AFAIK there are ways to add custom claims to the token, but lets chat w/ @James Harton and see what he says I haven't looked at that specific code in a while I wasn't trying to say we should stop you from doing that, ultimately you should be able to use the token how you want 🙂 I was just making sure you knew what you were getting into There is definitely code to add custom claims to the token, but maybe we'll need to add a callback for you to introduce custom claims into tokens generated for users. It would be a pretty easy change for us to make.
Vahagn
VahagnOP•5mo ago
Yeah. I like that about Ash a lot that it does not stop me doing anything, it just hase some idiomatic ways of handling stuff so you can expect some project to project or domain to domain consistency
ZachDaniel
ZachDaniel•5mo ago
Are you using LiveView? or is this just for an API?
Vahagn
VahagnOP•5mo ago
Just for API
ZachDaniel
ZachDaniel•5mo ago
Gotcha. So there is one troublesome issue here which only affects LiveView, which is basically "transferring" these claims adds extra work But for the other things its easy
Vahagn
VahagnOP•5mo ago
I just wonder if I only change the get_token part to preload some relation (through a manually overriden module, nor some idiomatic way) would it be enough?
ZachDaniel
ZachDaniel•5mo ago
ah, nvm we have the same problem So, after load_from_bearer
Vahagn
VahagnOP•5mo ago
I will look into some minor changes in Authentication lib, submit my work to #showcase and make a PR if you like what I made
ZachDaniel
ZachDaniel•5mo ago
you could add your own plug that looks at the tokens and then modifies the assigned user thats the easiest way for your current desires but having something in AA itself would make perfect sense Sounds great!
Vahagn
VahagnOP•5mo ago
just a config that says what else to preload into Token, before putting in JWT
ZachDaniel
ZachDaniel•5mo ago
It depends, not everyone has tokens being stored etc. We want to configure a function of user -> extra claims
Vahagn
VahagnOP•5mo ago
I saw something called extra claims. I will look into it. Maybe I will only need to submit a documentation update
ZachDaniel
ZachDaniel•5mo ago
GitHub
improvement: allow configuring extra claims for a user by zachdanie...
This is only half done. We still need to do one thing which is to modify the store_in_session helper to take opts and store the claims in the session so that they can be added to the user in LV ha...
ZachDaniel
ZachDaniel•5mo ago
This is the first half of the work But I don't have time to finish it up Perhaps you can get it over the line 😅
Vahagn
VahagnOP•5mo ago
I didn't dig a lot into LiveView to fully cover that part. I will work on it to support basic API routes. Probably will need a bit tuning in the end But it is a very good starting point for me. thanks.
jart
jart•5mo ago
I think what we want to do is add some options to the GenerateTokenChange to allow extra claims to be specified in the changeset somehow. Maybe private arguments? Not sure. In terms of validating it you can replace load_from_bearer with your own version that calls the verify token code in AA and then verifies your extra claims and puts them in the conn. If you want specific relationships or calculations loaded at verify time you can augment the generated get_by_subject action.
ZachDaniel
ZachDaniel•5mo ago
🤔 thats an interesting way to do it. Did you see the (partial) approach in my PR that lets you configure it?
jart
jart•5mo ago
Not yet. I’ve added it to my list of things to look at this morning.
Vahagn
VahagnOP•5mo ago
@@ -109,9 +109,9 @@ defmodule AshAuthentication.Jwt do

default_claims = Config.default_claims(resource, action_opts)
signer = Config.token_signer(resource, opts, context)
+ more_extra_claims = extra_claims_for_user(purpose, user, opts)

- with {:ok, more_extra_claims} <- extra_claims_for_user(purpose, user, opts),
- {:ok, token, claims} <-
+ with {:ok, token, claims} <-
Joken.generate_and_sign(
default_claims,
Map.merge(extra_claims, more_extra_claims),
@@ -109,9 +109,9 @@ defmodule AshAuthentication.Jwt do

default_claims = Config.default_claims(resource, action_opts)
signer = Config.token_signer(resource, opts, context)
+ more_extra_claims = extra_claims_for_user(purpose, user, opts)

- with {:ok, more_extra_claims} <- extra_claims_for_user(purpose, user, opts),
- {:ok, token, claims} <-
+ with {:ok, token, claims} <-
Joken.generate_and_sign(
default_claims,
Map.merge(extra_claims, more_extra_claims),
this is my only change from your PR that got it working. It was actually ready do be specified like this
tokens do
enabled? true
token_resource Aldente.Accounts.Token
signing_secret Aldente.Secrets
store_all_tokens? true
require_token_presence_for_authentication? true
extra_claims &get_extra_claims/2
end
tokens do
enabled? true
token_resource Aldente.Accounts.Token
signing_secret Aldente.Secrets
store_all_tokens? true
require_token_presence_for_authentication? true
extra_claims &get_extra_claims/2
end
I am guessing you wanted the extra claims to be inside Token.extra_data, which did not work :)))))
ZachDaniel
ZachDaniel•5mo ago
No need to have it in extra_data So that should work for you but before we actually merge that PR we have to handle saving the extra claims in the session
Vahagn
VahagnOP•5mo ago
I just don't understand why do we need a TokenResource for JWT's, how should get_token work if I chose store_all_tokens false So if I don't need to save it in extra_data, then they are only accessible when decoding JWT right?
ZachDaniel
ZachDaniel•5mo ago
The entire JWT is stored in the database if you're using store_all_tokens? true if not then correct they would only be available then but also: our choice of using JWTs was not because we believe that storing permissions authoritatively in JWTs is a good idea for most use cases.
Vahagn
VahagnOP•5mo ago
And how should we get that from TokenResource? something like :get_extra_claims action on token?
ZachDaniel
ZachDaniel•5mo ago
I always store all tokens, which allows for very important security related features like: 1. list active sessions 2. revoke all sessions (critical security feature) 3. (with your own custom logic added) detecting suspicious sessions @Vahagn I'm a bit confused The token gets handed to us from the client We shouldn't need to get the claims from the token should we?
Vahagn
VahagnOP•5mo ago
I thought I can get all the claims from token, to not make any DB calls In that case I will need refresh tokens to revoke all sessions (will work with a delay, after JWT expires)
ZachDaniel
ZachDaniel•5mo ago
What I mean is that the token is in the header You shouldn't need to extract the claims from the token resource
Vahagn
VahagnOP•5mo ago
I think I will move along with just loading user memberships when I resolve the actor, it will be easier to manage in the future. I don't think I will need the extra_claims for now

Did you find this page helpful?