Skip to content

Instantly share code, notes, and snippets.

@andrewlinfoot
Created October 10, 2018 17:55
Show Gist options
  • Save andrewlinfoot/21cfd199725eb606ca7d410c5b05ebe2 to your computer and use it in GitHub Desktop.
Save andrewlinfoot/21cfd199725eb606ca7d410c5b05ebe2 to your computer and use it in GitHub Desktop.
import {
AuthenticationError,
ForbiddenError,
SchemaDirectiveVisitor
} from 'apollo-server-express';
import { defaultFieldResolver } from 'graphql';
import * as jwt from 'jsonwebtoken';
import { JWT_SECRET } from '../../constants';
import { log } from '../../utils';
// Ranked with highest access level first
export const roles = ['ADMIN', 'APPLICANT'];
export function hasRequiredRole({ user, requiredRole }) {
const userRolePriority = roles.indexOf(user.role);
const requiredRolePriority = roles.indexOf(requiredRole);
return userRolePriority <= requiredRolePriority;
}
async function getUser({ context }) {
const { headers, db } = context;
const authHeader = headers.authorization || headers.Authorization || '';
const requestToken = authHeader.replace('Bearer ', '');
if (context.user !== undefined) {
// This wrapper has already been run for the request.
// We do not need to query the db again.
return context.user;
}
log('Verifying request token');
let token;
try {
token = await jwt.verify(requestToken, JWT_SECRET);
} catch (err) {
// Do not edit this message value. The client depends on this
// in Apply__PrivateRoute.re. This is a temporary hack until we
// figure out how to handle specific error codes in reason-apollo.
throw new AuthenticationError('Invalid token');
}
const userId = token.userId;
log('Checking db for user with id %s', userId);
const user = await db.getUser({ id: userId });
if (user === null) {
log('No user found with id %s', userId);
throw new AuthenticationError('User not found');
}
return user;
}
class AuthDirective extends SchemaDirectiveVisitor {
visitObject(type) {
this.ensureFieldsWrapped(type);
type._requiredAuthRole = this.args.requires;
}
// Visitor methods for nested types like fields and arguments
// also receive a details object that provides information about
// the parent and grandparent types.
visitFieldDefinition(field, details) {
this.ensureFieldsWrapped(details.objectType);
field._requiredAuthRole = this.args.requires;
}
ensureFieldsWrapped(objectType) {
// Mark the GraphQLObjectType object to avoid re-wrapping:
if (objectType._authFieldsWrapped) {
return;
}
objectType._authFieldsWrapped = true;
const fields = objectType.getFields();
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
const { resolve = defaultFieldResolver } = field;
field.resolve = async function(...args) {
// Get the required Role from the field first, falling back
// to the objectType if no Role is required by the field:
const requiredRole =
field._requiredAuthRole || objectType._requiredAuthRole;
if (!requiredRole) {
return resolve.apply(this, args);
}
const context = args[2];
const user = await getUser({ context });
if (!hasRequiredRole({ user, requiredRole })) {
log("User doesn't have the %s permissions", requiredRole);
throw new ForbiddenError(
"User doesn't have the required permissions"
);
}
log('User found, request authorized');
context.user = user;
return resolve.apply(this, args);
};
});
}
}
export default AuthDirective;
/*
Here is an example of how the directive can be used to have different authentication levels for different mutations
*/
type Mutation {
login(email: String!): Boolean! # Send magic sign-in link email
exchangeEmailToken: AuthenticatedUser! @auth(requires: APPLICANT) # Swap out magic sign-in token for a longer lived token
sendConfirmationInvoice(
confirmationInvoiceInput: ConfirmationInvoiceInput!
): Application! @auth(requires: ADMIN)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment