Apollo-server: Async context

Created on 29 Nov 2016  路  7Comments  路  Source: apollographql/apollo-server

I need to authenticate my client requests using a JWT token sent as an "Authentication" header added by my client's middleware. I'm using a function to create the resolver's context object from the request [http://dev.apollodata.com/tools/graphql-server/setup.html#options-function].

But decoding a JWT token is typically an async operation (returns a promise) and the context variable doesn't support this.

~~
graphqlExpress(request => ({
schema: typeDefinitionArray,
context: () => ({
userId: verifyIdToken(request); // DOESN'T WORK
})
}))
~
~

The workaround is to just add the header value to the context, but then each resolver: a) has to individually resolve it; b) all resolvers now become asynchronous. Seems like supporting promises directly in the context would be useful? Bonus to allow rejection to happen here if the user is not authenticated.

Most helpful comment

@richburdon you can return a promise for an options object instead of an options object.

From the docs:

Alternatively, GraphQL Server can accept a function which takes the request as input and returns a GraphQLOptions object or a promise that resolves to one

All 7 comments

Granted, I am no expert. But, is accessing data really an all or nothing thing, ever? I would venture to say, in most cases, probably not. If it is a web application, there is always going to be something available to view even for an anonymous user at some point. Thus yes, resolvers should individually check on "viewable" permissions. And, in general, most resolvers should be asynchronous too, so data can be fetched in parallel saving time. So, I think what you call a workaround is the solution. 馃槃

Rejection of the user up front should be done in your authentication process.

This is a great article on the subject.

https://dev-blog.apollodata.com/a-guide-to-authentication-in-graphql-e002a4039d1

Scott

Hi @smolinari, appreciate the comments (and link) but:

1). What do you mean by rejection of the user up front? Anything can make an XMLHTTP POST call to the /graphql endpoint; you could use curl from your laptop. The endpoint needs to be securely authenticated.

2). A GraphQL query may invoke many resolvers, so doing this check for each seems expensive (the JWT decoder will cache the token, but it seems like the wrong place to do this check).

OK, so I think I figured out a better way using async/await.

~~~~
function getUser(request) {
return new Promise() { DO ASYNC HERE };
}

router.use(''graphql', graphqlExpress(async function(request) {
let user = await getUser(request)
return {
context: { user }
}
}));
~~~~

What do you mean by rejection of the user up front?

I meant, if a user needs to be authenticated, that step should have been done, before the request is passed on to GraphQL.

2). A GraphQL query may invoke many resolvers, so doing this check for each seems expensive (the JWT decoder will cache the token, but it seems like the wrong place to do this check).

If the user is already authenticated, then it would be a validated user passed into the resolvers. There is no more checking on the user's authenticity at the resolver level. What happens next in the resolver is business logic for authorization to answer the question, "can this particular user see the data available through this resolver?" That question most definitely needs to be answered at that point too, and for each resolver (except the case of a completely public API).

Scott

@richburdon you can return a promise for an options object instead of an options object.

From the docs:

Alternatively, GraphQL Server can accept a function which takes the request as input and returns a GraphQLOptions object or a promise that resolves to one

Thanks very much @helfer I missed that.

@helfer: can you link to where you found that, or better yet, to today's doc? Googling for "Alternatively, GraphQL Server can accept a function which takes the request as input and returns a GraphQLOptions object or a promise that resolves to one" find this issue as the best match.

Here is that info from the docs: https://www.apollographql.com/docs/apollo-server/data/resolvers/#the-context-argument

Context initialization can be asynchronous, allowing database connections and other operations to complete:

context: async () => ({
  db: await client.connect(),
})
Was this page helpful?
0 / 5 - 0 ratings