Created
October 10, 2018 17:55
-
-
Save andrewlinfoot/21cfd199725eb606ca7d410c5b05ebe2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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