Handling Authentication and Authorization in GraphQL
Ryan Chenkie
@ryanchenkie
- Developer Advocate @ Auth0
- Google Developer Expert
- Angularcasts.io,
- GraphQLWorkshops.com
Phenix
Phenix from Slack
Provisioning tools are centered around permissions
How do we add authentication and authorization?
However we want!
GraphQL doesn't have an opinion about auth
But that's not very helpful
Let's look at some ways it can be done
1. Wrapping Resolvers
2. Custom Directives
API auth needs to answer a few questions
- Is the requested data private?
- Does the request contain authentication/authorization information?
- Is that information valid?
Getting users to prove their identity is another topic
Typical Auth in REST
const authMiddleware = (req, res, next) => {
if (userIsAuthenticated(req)) {
next();
} else {
res.status(401).send("Sorry, you're not allowed here!");
}
app.get('/private-data', authMiddleware, (req, res) => {
res.send(somePrivateData);
});
We want something similar in GraphQL
But not this...
const authMiddleware = ...
app.use('/graphql', authMiddleware, graphqlHTTP({
schema
});
We need something that
- Isn't a catch-all
- Gives us info on the authenticated user
- Allows us to handle auth errors appropriately
How do we get there?
1. Wrapping Resolvers
Two Steps to Auth
- Verify authentication info is legit
- Use it to make authorization decisions
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);
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 repetitive
We can wrap the resolver instead
A resolver which needs to check scopes
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
);
}
}
Let's get smarter about checking the JWT
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) {
throw new AuthorizationError();
}
}
2. Custom Directives
What if we want to limit access to specific fields?
We can use custom directives
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 | FIELD
type Article {
id: ID!
author: String!
reviewerComments: [ReviewerComment] @hasScope(scope: ["read:comments"])
}
type Query {
allArticles: [Article] @isAuthenticated
}
`;
Defining Custom Directives
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 => {
...
});
});
};
github.com/chenkie/graphql-auth
With this pattern we get
- A clean API
- Possibility for per-field authorization checks
- Resolver-level authentication checks
There are many ways to do auth in GraphQL
Try a few, see what works best, combine them if you want
However we want!
Thank you!
@ryanchenkie
bit.ly/graphql-auth
graphqlworkshops.com
Handling Authentication and Authorization in GraphQL
By Ryan Chenkie
Handling Authentication and Authorization in GraphQL
Auth for GraphQL can be tricky. It's easy to lock down the entire endpoint but this is often too much. How do we get more specific? Let's talk about the available options and best practices for auth in GraphQL. You'll come away knowing everything you need to get a working auth solution in place
- 3,113