Dmitry Minkovsky Dmitry Minkovsky - 26 days ago 8
Javascript Question

Authentication and Access Control with Relay

The official line from Facebook is that Relay is "intentionally agnostic about authentication mechanisms." In all the examples in the Relay repository, authentication and access control are a separate concern. In practice, I have not found a simple way to implement this separation.

The examples provided in the Relay repository all have root schemas with a

viewer
field that assumes there is one user. And that user has access to everything.

However, in reality, an application has has many users and each user has different degrees of access to each node.

Suppose I have this schema in JavaScript:

export const Schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: () => ({
node: nodeField,
user: {
type: new GraphQLObjectType({
name: 'User',
args: {
// The `id` of the user being queried for
id: { type: new GraphQLNonNull(GraphQLID) },
// Identity the user who is querying
session: { type: new GraphQLInputObjectType({ ... }) },
},
resolve: (_, { id, session }) => {
// Given `session, get user with `id`
return data.getUser({ id, session });
}
fields: () => ({
name: {
type: GraphQLString,
resolve: user => {
// Does `session` have access to this user's
// name?
user.name
}
}
})
})
}
})
})
});


Some users are entirely private from the perspective of the querying user. Other users might only expose certain fields to the querying user. So to get a user, the client must not only provide the user ID they are querying for, but they must also identify themselves so that access control can occur.

This seems to quickly get complicated as the need to control access trickles down the graph.

Furthermore, I need to control access for every root query, like
nodeField
. I need to make sure that every node implementing
nodeInterface
.

All of this seems like a lot of repetitive work. Are there any known patterns for simplifying this? Am I thinking about this incorrectly?

Answer

I found that handling authentication is easy if you make use of the GraphQL rootValue, which is passed to the execution engine when the query is executed against the schema. This value is available at all levels of execution and is useful for storing an access token or whatever identifies the current user.

If you're using the express-graphql middleware, you can load the session in a middleware preceding the GraphQL middleware and then configure the GraphQL middleware to place that session into the root value:

function getSession(req, res, next) {
  loadSession(req).then(session => {
    req.session = session;
    next();
  }).catch(
    res.sendStatus(400);
  );
}

app.use('/graphql', getSession, graphqlHTTP(({ session }) => ({
  schema: schema,
  rootValue: { session }
})));

This session is then available at any depth in the schema:

new GraphQLObjectType({
  name: 'MyType',
  fields: {
    myField: {
      type: GraphQLString,
      resolve(parentValue, _, { rootValue: { session } }) {
        // use `session` here
      }
    }
  }
});

You can pair this with "viewer-oriented" data loading to achieve access control. Check out https://github.com/facebook/dataloader which helps create this kind of data loading object and provides batching and caching.

function createLoaders(authToken) {
  return {
    users: new DataLoader(ids => genUsers(authToken, ids)),
    cdnUrls: new DataLoader(rawUrls => genCdnUrls(authToken, rawUrls)),
    stories: new DataLoader(keys => genStories(authToken, keys)),
  };
}