RFC: Authorization
mxstbr opened this issue · comments
Summary
We are considering adding authorization as a first-class citizen to Fuse.js.
Proposed Solution
None yet. It's unclear whether there is a good use case for this, as the common scenario will be that the underlying microservices & third-party APIs already have to handle authentication & authorization.
However, in my personal experience, handling authorization at the GraphQL level (in my case simply through making sure that each resolver has an authorization check) was terrific to work with and kept our system well secure enough. We could e.g. require each node
to have an authorize
check that explicitly has to mark things as public via authorize: () => true
so that it's explicitly note.
Note: Pothos has two related plugins: Authz & Scope Auth.
I like the authorize function but how do you plan to handle field level authorization?
Nothing prevents us from having t.field({ authorize })
I however am more concerned with the authorization inheritance problems that could show themselves. For example, when someone submits an order, that's their order only they can look at it so on the node
we would authorize
contextually. Let's imagine that the above API is also used by super-users that go about shipping orders, they aren't that user so there's an additional option for authorization all of a sudden that becomes contextual to the field that has produced this Order
.
This means that node()
might not be the best place to define authorization
or it could be a good place to define the least restrictive authorization and define the most restrictive authorization on a field-level. 😅 to leave my thoughts out here
Yes @JoviDeCroock . Your example is right, most of time we keep the same endpoint and handle the authorization if you are admin ou owner of this resource to avoid duplication of code.
Node could be a good place if it is getting the context of the user and the information about the field.
Field level authorization is mostly for hidding. Let's take the email field of user for example,
- if he make it public you can see it it
- if he make it private but it is the owner or he is admin you can see it
- else we return null
Coming back to this, I think I personally prefer us using the pothos
auth-scopes plugin as that allows us to have a convenient battle-tested way (we use this at stellate!) of defining authorization on a by-field, by-type, ... level.
The issue we'll face here is the typings aspect as we did fork the SchemaBuilder
creation to introduce our own opinions. Maybe we can override it through global-types, this is to-test.
The general thinking here would be that we give the power to the user to define a set of scopes with a function
import { defineScopes } from 'fuse'
defineScopes((ctx) => ({
isLoggedIn: () => !!ctx.user,
isAllowedToReadLaunch: async (launchId) => {
// do async logic
return true
}
})
Now folks can add this to their field and/or type-definition by means of authScopes: { isLoggedIn: true, isAllowedToReadLaunch: l => l.id }
where we can also specify a function to