Just getting into multi-tenancy and the Ash support for it. My schema looks like:
Organisation
(global, tenant entity)
Identity
(global) —(has many)—> User
(tenant)
User
(tenant) —(has many)—> Project
(tenant)
Token
(global)
So if a User
has a record, it means the Identity
belongs to the same Organisation
as the User
record. Authentication is performed for Identity
.
Is this reasonable so far?
Now I want to add the API key strategy. But the assumptions are based on a uni-tenant configuration, it seems, adding another strategy to what my schema calls Identity
now. But I need the API keys to be associated to an organisation as well, so they need to work for User
.
Can I just enable extensions: [AshAuthentication]
for User
? Of course I tried, but Ash complains:
** (Spark.Error.DslError) authentication -> tokens -> enabled?:
The `:api_key` authentication strategy requires tokens be enabled.
But “tokens” means JWT tokens, no?
What is the recommended approach here? Or is there a flaw in my foundation?
Thanks again to the Ash team for the amazing work! 
2 Likes
Tokens are not necessarily JWTs. It is the token resource that we use to store api keys.
Actually, let me double check myself on that front.
Ok, tried with latest version. Now it says for User
:
** (EXIT from #PID<0.94.0>) an exception was raised:
** (Spark.Error.DslError) authentication -> session_identifier:
Must set `authentication.session_identifier` to either `:jti` or `:unsafe`,
unless `authentication.tokens.require_token_presence_for_authentication?` is set to `true`.
If you are seeing this error while upgrading ash_authentication, be aware that
updating this setting will log out all of your users.
When set to `:unsafe`, tokens are not revoked when the user logs out.
When set to `:jti`, we use this information to revoke tokens on logout.
We suggest setting `authentication.tokens.require_token_presence_for_authentication?` to `true`
to ensure that tokens are always present during authentication, which makes this option unnecessary.
Changing either of these settings will log out all of your users.
So I added
tokens do
require_token_presence_for_authentication? true
end
(full user.ex)
leading to:
authentication -> tokens:
** (Spark.Options.ValidationError) required :token_resource option not found, received options: [:require_token_presence_for_authentication?]
It seems, it does rely on “tokens” somehow… 
Looks like just another check I need to fix 
Yay, was able to create a token!
Thank you, Zach!
Found a GraphQL error message that might deserve proper handling. Just FYI, optional cosmetics for now… 
{
"data": {
"createProject": {
"errors": [
{
"message": "something went wrong. Unique error id: `892f6ed9-fce4-42e4-8f0c-827ffcf46b19`"
}
],
"result": null
}
}
}
[warning] `892f6ed9-fce4-42e4-8f0c-827ffcf46b19`: AshGraphql.Error not implemented for error:
** (Ash.Error.Invalid.TenantRequired) Queries against the Zeitmeister.TimeTracking.Project resource require a tenant to be specified
(ash 3.5.24) lib/ash/error/invalid/tenant_required.ex:4: Ash.Error.Invalid.TenantRequired.exception/1
(ash 3.5.24) lib/ash/actions/create/create.ex:602: Ash.Actions.Create.set_tenant/1
Just need to fix my scoping plug now, I guess…
1 Like
Hmm…that one is interesting. So the reason we did that is that externally the term for a tenant might be something other than “tenant”, so it would make sense to have you implement that yourself. There is a guide on doing that IIRC.
Ok, I introduced some denormalisation to avoid walking the tenant tables:
Organisation
(global, tenant entity) —(has many)—> ApiKey
(global)
User
(tenant) —(has many)—> ApiKey
(global)
I want to authenticate User
(tenant) via ApiKey
(global):
defmodule User do
actions do
read :sign_in_with_api_key do
argument :api_key, :string, allow_nil?: false
prepare AshAuthentication.Strategy.ApiKey.SignInPreparation
end
# [...]
end
# [...]
end
User
is only accessible via tenant. Tenant can be derived directly from ApiKey
.
AshAuthentication says:
22:49:59.723 request_id=GE7Y6Ni6AJHyb58AAABE [warning] Authentication failed:
Bread Crumbs:
> Error returned from: Timetracker.Accounts.User.sign_in_with_api_key
Invalid Error
* Queries against the Timetracker.Accounts.User resource require a tenant to be specified
(ash 3.5.24) lib/ash/error/invalid/tenant_required.ex:4: Ash.Error.Invalid.TenantRequired.exception/1
(ash 3.5.24) lib/ash/actions/read/read.ex:2564: Ash.Actions.Read.validate_multitenancy/1
(ash 3.5.24) lib/ash/actions/read/read.ex:2433: Ash.Actions.Read.handle_multitenancy/1
(ash 3.5.24) lib/ash/actions/read/read.ex:474: Ash.Actions.Read.do_read/5
(ash 3.5.24) lib/ash/actions/read/read.ex:330: Ash.Actions.Read.do_run/3
(ash 3.5.24) lib/ash/actions/read/read.ex:89: anonymous fn/3 in Ash.Actions.Read.run/3
(ash 3.5.24) lib/ash/actions/read/read.ex:88: Ash.Actions.Read.run/3
(ash 3.5.24) lib/ash.ex:2760: Ash.read/2
(ash_authentication 4.9.5) lib/ash_authentication/strategies/api_key/actions.ex:26: AshAuthentication.Strategy.ApiKey.Actions.sign_in/3
(ash_authentication 4.9.5) lib/ash_authentication/strategies/api_key/plug.ex:116: AshAuthentication.Strategy.ApiKey.Plug.call/2
Which is a valid complaint…
What’s a good way to set the tenant? Maybe I could sneak in another before_action
to validate (or at least identify) the API key and fetch Organisation
via ApiKey
, set the context, and let AshAuth validate it again. But that would be double work and I’m not sure it would even work, it seems, AshAuth (or tenant read actions in general) bails out before it even runs (which makes sense).
Copying AshAuthentication.Strategy.ApiKey.SignInPreparation
and patching it to do that is not an option either, as AshAuth becomes mutinous unless exactly that preparation is present. And again, wouldn’t run anyway.
Ideas welcome. 
Ah, interesting. I think what you can do is allow bypassing multi tenancy for that read action, since every api key will uniquely be uniquely identified.
read :sign_in_with_api_key do
argument :api_key, :string, allow_nil?: false
prepare AshAuthentication.Strategy.ApiKey.SignInPreparation
multitenancy :bypass
end
Hmm, as long as User
is not global, the multitenancy :bypass
does not help, right?
My idea was to keep it like that but tell Ash (or make it figure out) the correct tenant before trying to read the actual user record in the end.
Anyway, for now I completely removed multitenancy
for User
– should be good enough to get started. 
Right
yeah, we may need to add more consideration there, like encode the tenant into the api key in some way. Looks like a scenario I didn’t think through fully 
Wouldn’t it be a way to extend AshAuthentication.Strategy.ApiKey.SignInPreparation
to
- Check if the API key resource (here:
ApiKey
) belongs to the tenant entity resource (Organisation
)
- If so, derive tenant from there and set the right tenant before reading the authenticated resource (
User
)?
Might play around some more tomorrow…
Ash doesn’t know about a “multitenancy resource”. It would likely require some kind of configuration/hook of some kind.