Documentation
Building your API (server)
Authorization

Authorization

In some cases you might not wish to fully rely on your datasource to perform authorization like in the case of a database or you want to optimise to terminate requests as soon as possible.

In these cases you can use auth-scopes to perform authorization both on the field- as well as type-level.

Setting up

To use the auth-scopes we'll need to create a new file in types/ where we'll define the type for our scope as well as how to resolve it.

import 'fuse'
import { defineAuthScopes, Scopes } from 'fuse'
 
declare module 'fuse' {
  export interface Scopes {
    isLoggedIn: boolean
  }
}
 
defineAuthScopes<Scopes>((ctx) => ({
  isLoggedIn: !!ctx.user,
  isAdmin: !!ctx.admin,
  canAccessUser: (userId) => ctx.allowedIds.includes(userId)
}))

In the above we define three scopes. The scopes isLoggedIn and isAdmin allow us to restrict access to types or fields based on the users role - anonymous, logged in and/or admin. The scope canAccessUser takes a parameter. The parameter allows us to dynamically restrict access to individual instances of a type. For example a user should be able to access Query.user for them selves and their colleagues, but not for user ids outside of their own organisation.

An important note here is that we are using the context to setup these scopes. Therefore we must define the user and allowedIds on the context when we are executing our request.

Using scopes

Now that we have defined our scopes we can use them in our schema

import { node } from 'fuse';
 
const UserNode = node<
  UserSource,
>({
  name: 'User',
  // These functions allow you to do full logic i.e. you can also return
  // scopes conditionally based on some super-admin privilege where you
  // just do return true; instead of return {}
  authScopes: (parent, args) => ({ canAccessUser: parent.id || args.id }),
  load: async (ids) => getUsers(ids),
  fields: (t) => ({
    name: t.exposeString('name'),
    avatarUrl: t.exposeString('avatarUrl'),
    secretField: t.exposeString('secretField', {
      authScopes: { isAdmin: true },
    }),
  }),
})

Now anytime we query Query.node(id: ID!) or Query.user(id: ID!) we will check if the consumer of said request is allowed to access a given id. When we request secretField we will check if the consumer is an admin, if they're not but they are allowed to access the user because it's a nullable field it will show up as null but the other fields will be resolved correctly.

We can also define our scopes on the query-field level:

import { addQueryFields } from 'fuse';
 
addQueryFields((t) => ({
  me: t.field({
    type: 'User',
    authScopes: {
      isLoggedIn: true,
    },
    resolve: () => {
      return true
    },
  }),
}))

Note that using complex values as an input to auth-scope functions will result in lower performance as we can't generate effective cache-keys for them.