最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Authentication and Access Control with Relay - Stack Overflow

programmeradmin6浏览0评论

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 plicated 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?

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 plicated 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?

Share Improve this question edited May 23, 2017 at 12:09 CommunityBot 11 silver badge asked Nov 5, 2015 at 17:18 Dmitry MinkovskyDmitry Minkovsky 38.1k29 gold badges123 silver badges168 bronze badges 4
  • 1 I think it would be really cool if there was some middleware in Relay that sat above the execution engine and rewrote queries AST based on session information. – Dmitry Minkovsky Commented Nov 5, 2015 at 17:25
  • 1 Did you ever get a good example/answer? I am looking for information on token authentication (no session) with relay but it is hard to find anything – irl_irl Commented Feb 8, 2016 at 9:12
  • 1 @GreenRails not here but I figured out how to do it. It's pretty nice! Basically the key for me was figuring out that you can put things into the GraphQL "rootValue", which is available at all levels of resolution. If you're using the express middleware, it's done like this: gist.github./dminkovsky/…. Same can be done for any implementation. Then, per the answer below, you can also take a 'viewer-oriented' approach to loading data to assist in ACL. github./facebook/dataloader is a good helper tool. – Dmitry Minkovsky Commented Feb 8, 2016 at 13:22
  • @GreenRails just added an answer – Dmitry Minkovsky Commented Feb 8, 2016 at 13:36
Add a ment  | 

3 Answers 3

Reset to default 7

Different applications have very different requirements for the form of access control, so baking something into the basic Relay framework or GraphQL reference implementation probably doesn't make sense.

An approach that I have seen pretty successful is to bake the privacy/access control into the data model/data loader framework. Every time you load an object, you wouldn't just load it by id, but also provide the context of the viewer. If the viewer cannot see the object, it would fail to load as if it doesn't exist to prevent even leaking the existence of the object. The object also retains the viewer context and certain fields might have restricted access that are checked before being returned from the object. Baking this in the lower level data loading mechanism helps to ensure that bugs in higher level product / GraphQL code doesn't leak private data.

In a concrete example, I might not be allowed to see some User, because he has blocked me. You might be allowed to see him in general, but no his email, since you're not friends with him.

In code something like this:

var viewer = new Viewer(getLoggedInUser());
User.load(id, viewer).then(
  (user) => console.log("User name:", user.name),
  (error) => console.log("User does not exist or you don't have access.")
)

Trying to implement the visibility on GraphQL level has lots of potential to leak information. Think of the many way to access a user in GraphQL implementation for Facebook:

node($userID) { name }
node($postID) { author { name } }
node($postID) { likers { name } }
node($otherUserID) { friends { name } }

All of these queries could load a user's name and if the user has blocked you, none of them should return the user or it's name. Having the access control on all these fields and not forgetting the check anywhere is a recipe for missing the check somewhere.

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./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)),
  };
}

If anyone has problems with this topic: I made an example repo for Relay/GraphQL/express authentication based on dimadima's answer. It saves session data (userId and role) in a cookie using express middleware and a GraphQL Mutation

发布评论

评论列表(0)

  1. 暂无评论