@felixfbecker on October 26, 2017
Ryan Chenkie (@ryanchenkie) is a developer advocate at Auth0, a Google Developer Expert and teaches a lot about Angular and GraphQL.
Phoenix is a tool built by Auth0 that allows new employees to get permissions to the GitHub org, npm org, etc. by asking the Phoenix bot on Slack.
The usual response to this question from the GraphQL community is "However you want". The reason for that is that the GraphQL spec is not opnionated about auth.
API auth needs to answer a few questions:
Authentication in REST could look like this in an Express API:
We want something similar in GraphQL, but not like this:
First we need to verify authentication:
const jwt = require('express-jwt');
const jwtDecode = require('jwt-decode');
const jwtMiddleware = jwt({ secret: 'some-strong-secret-key' });
const getUserFromJwt = (req, res, next) => {
const authHeader = req.headers.authorization;
req.user = jwtDecode(authHeader);
next();
}
app.use(jwtMiddleware);
app.use(getUserFromJwt);
This example extracts a JWT from the request and attaches it to the request.
Now we can use the payload in our resolvers:
const resolvers = {
Query: {
articlesByAuthor: (_, args, context) => {
return model.getArticles(context.user.sub);
}
}
}
We can also do authorization checks:
const resolvers = {
Query: {
articlesByAuthor: (_, args, context) => {
const scope = context.user.scope;
if (scope.includes('read:articles')) {
return model.getArticles(context.user.id);
}
}
}
}
This can get a bit repitive. One option to avoid that is to wrap resolvers:
const checkScopeAndResolve = (scope, expectedScope, controller) => {
const hasScope = scope.includes(expectedScope);
if (!expectedScopes.length || hasScope) {
return controller.apply(this);
}
}
const controller = model.getArticles(context.user.id);
const resolvers = {
Query: {
articlesByAuthor: (_, args, context)
=> checkScopeAndResolve(
context.user.scope,
['read:articles'],
controller
);
}
}
We can check the JWT in the wrapper:
import { createError } from'apollo-errors';
import jwt from'jsonwebtoken';
const AuthorizationError = createError('AuthorizationError', {
message: 'You are not authorized!'
});
const checkScopeAndResolve = (context, expectedScope, controller) => {
const token = context.headers.authorization;
try {
const jwtPayload = jwt.verify(token.replace('Bearer ', ''), secretKey);
const hasScope = jwtPayload.scope.includes(expectedScope);
if (!expectedScopes.length || hasScope) {
return controller.apply(this);
}
} catch (err) {
thrownew AuthorizationError();
}
}
image
What if we want to limit access to specific fields? Custom directives give our queries more power:
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
We can use custom directives on our server:
const typeDefs = `
directive @isAuthenticated on QUERY | FIELD
directive @hasScope(scope: [String]) on QUERY | FIELDtypeArticle {
id: ID!
author: String!
reviewerComments: [ReviewerComment] @hasScope(scope: ["read:comments"])
}typeQuery {
allArticles: [Article] @isAuthenticated
}
`;
which are defined like this:
const directiveResolvers = {
isAuthenticated(result, source, args, context) {
const token = context.headers.authorization;
// ...
},
hasScope(result, source, args, context) {
const token = context.headers.authorization;
// ...
}
};
const attachDirectives = schema => {
forEachField(schema, field => {
const directives = field.astNode.directives;
directives.forEach(directive => {
...
});
});
};
This is what https://github.com/chenkie/graphql-auth does for you.
With this pattern we get
Everyone has its own opinion about how to do auth in GraphQL, for example some say that the API layer shouldn't have any concept of auth at all.
Try a few, see what works best, combine them if you want.