Skip to content
This repository has been archived by the owner on Oct 21, 2020. It is now read-only.

Add auth options 🎉 #58

Merged
merged 17 commits into from
Apr 28, 2018
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions auth/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import jwt from 'jsonwebtoken';
import debug from 'debug';

import { AuthorizationError } from '../graphql/errors';

const log = debug('fcc:auth');
const { JWT_CERT, NODE_ENV } = process.env;

export function verifyWebToken(ctx) {
log('Verifying token');
const token = ctx && ctx.headers && ctx.headers.authorization;
if (!token) {
throw new AuthorizationError({
message: 'You must supply a JSON Web Token for authorization!'
});
}
let decoded = null;
let error = null;
try {
decoded = jwt.verify(token.replace('Bearer ', ''), JWT_CERT, {
ignoreExpiration: NODE_ENV === 'test'

This comment was marked as off-topic.

});
} catch (err) {
error = err;
} finally {
return { decoded, error, isAuth: !!decoded };
}
}
7 changes: 4 additions & 3 deletions config/secrets.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
require('dotenv').config();

const { MONGODB_URL, GRAPHQL_ENDPOINT_URL } = process.env;
const { MONGODB_URL, GRAPHQL_ENDPOINT_URL, JWT_CERT } = process.env;

exports.getSecret = () => ({
MONGODB_URL,
GRAPHQL_ENDPOINT_URL
GRAPHQL_ENDPOINT_URL,
JWT_CERT,
MONGODB_URL
});
11 changes: 6 additions & 5 deletions dataLayer/mongo/user.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
const validator = require('validator');
const UserModel = require('../model/user.js');
const moment = require('moment');
const fs = require('fs');
import validator from 'validator';
import UserModel from '../model/user.js';
import debug from 'debug';

import { asyncErrorHandler } from '../../utils';

const log = debug('fcc:dataLayer:mongo:user');

function doesExist(Model, options) {
return Model.find(options).exec();
}

export function getUsers(args) {
export function getUsers(_, args = {}) {
return new Promise((resolve, reject) => {
UserModel.find(args)
.then(users => {
Expand Down
5 changes: 5 additions & 0 deletions graphql/errors/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createError } from 'apollo-errors';

export const AuthorizationError = createError('AuthorizationError', {
message: 'You are not authorized.'
});
62 changes: 62 additions & 0 deletions graphql/resolvers/directive.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* global expect beforeEach */
import sinon from 'sinon';
import sinonStubPromise from 'sinon-stub-promise';
import { createDirectives } from './directives';

sinonStubPromise(sinon);
const testErrorMsg = 'Test error message';
let nextSpy = sinon.spy();
let nextPromiseStub = sinon.stub().returnsPromise();
let authTrueStub = sinon.stub().returns({ isAuth: true });
let authFalseStub = sinon
.stub()
.returns({ isAuth: false, error: { message: testErrorMsg } });

beforeEach(() => {
authFalseStub.resetHistory();
authTrueStub.resetHistory();
nextSpy.resetHistory();
nextPromiseStub.resetHistory();
});

describe('isAuthenticatedOnField', () => {
it('should return null if authenication fails', () => {
const { isAuthenticatedOnField } = createDirectives(authFalseStub);
const secretValue = 'secret squirrel';
nextPromiseStub.resolves(secretValue);
const result = isAuthenticatedOnField(nextPromiseStub);

expect(authFalseStub.calledOnce).toBe(true);
expect(result.resolved).toBe(true);
expect(result.resolveValue).toBe(null);
});

it('should return the secretValue if authentication succeeds', () => {
const { isAuthenticatedOnField } = createDirectives(authTrueStub);
const secretValue = 'secret squirrel';
nextPromiseStub.resolves(secretValue);
const result = isAuthenticatedOnField(nextPromiseStub);

expect(authTrueStub.calledOnce).toBe(true);
expect(result.resolved).toBe(true);
expect(result.resolveValue).toBe(secretValue);
});
});

describe('isAuthenticatedOnQuery', () => {
it('should throw an error is auth fails', () => {
const { isAuthenticatedOnQuery } = createDirectives(authFalseStub);

expect(() => {
isAuthenticatedOnQuery(nextSpy);
}).toThrowError(testErrorMsg);
expect(nextSpy.called).toBe(false);
});

it('should call next if auth succeeds', () => {
const { isAuthenticatedOnQuery } = createDirectives(authTrueStub);
isAuthenticatedOnQuery(nextPromiseStub);

expect(nextPromiseStub.calledOnce).toBe(true);
});
});
30 changes: 30 additions & 0 deletions graphql/resolvers/directives.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AuthorizationError } from '../errors';
import { verifyWebToken as _verifyWebToken } from '../../auth';
import { asyncErrorHandler } from '../../utils';

/*
Interface: {
Directive: (
next: Resolver <Promise>
source: any <From Data Source>, Example <User Object>
args: any, passed to directive
ctx: Lambda context <Object>
) => <Promise> | <Error>
}
*/

export const createDirectives = (verifyWebToken = _verifyWebToken) => ({
isAuthenticatedOnField: (next, source, args, ctx) => {
const { isAuth } = verifyWebToken(ctx);
return asyncErrorHandler(next().then(result => (isAuth ? result : null)));
},
isAuthenticatedOnQuery: (next, source, args, ctx) => {
const { isAuth, error } = verifyWebToken(ctx);
if (isAuth) {
return asyncErrorHandler(next());
}
throw new AuthorizationError({
message: `You are not authorized, ${error.message}`

This comment was marked as off-topic.

});
}
});
1 change: 1 addition & 0 deletions graphql/resolvers/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mergeResolvers } from 'merge-graphql-schemas';
import { userResolvers } from './user';

export { createDirectives } from './directives';
export default mergeResolvers([userResolvers]);
4 changes: 2 additions & 2 deletions graphql/resolvers/user.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as dbUsers from '../../dataLayer/mongo/user';
import { createUser, getUsers } from '../../dataLayer/mongo/user';

export const userResolvers = {
Query: {
users: (_, args) => dbUsers.getUsers(args)
users: getUsers
},
Mutation: {
createUser: (_, { email }) => dbUsers.createUser(email)

This comment was marked as off-topic.

This comment was marked as off-topic.

Expand Down
2 changes: 1 addition & 1 deletion graphql/typeDefs/User/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ type Query {
_id: ID
name: String
email: String
): [User]
): [User] @isAuthenticatedOnQuery
}
`;
2 changes: 1 addition & 1 deletion graphql/typeDefs/User/type.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default `
type User {
_id: ID!
_id: ID @isAuthenticatedOnField
email: String
name: String
}
Expand Down
4 changes: 4 additions & 0 deletions graphql/typeDefs/directives/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default `
directive @isAuthenticatedOnField on FIELD | FIELD_DEFINITION
directive @isAuthenticatedOnQuery on FIELD | FIELD_DEFINITION
`;
3 changes: 2 additions & 1 deletion graphql/typeDefs/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mergeTypes } from 'merge-graphql-schemas';
import User from './User';
import directives from './directives';

export default mergeTypes([User]);
export default mergeTypes([User, directives]);
43 changes: 0 additions & 43 deletions gulpfile.js

This file was deleted.

65 changes: 42 additions & 23 deletions handler.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { graphqlLambda, graphiqlLambda } from 'apollo-server-lambda';
import { graphqlLambda } from 'apollo-server-lambda';
import lambdaPlayground from 'graphql-playground-middleware-lambda';
import { makeExecutableSchema } from 'graphql-tools';
import debug from 'debug';

import typeDefs from './graphql/typeDefs';
import resolvers from './graphql/resolvers';
import { default as resolvers, createDirectives } from './graphql/resolvers';
import connectToDatabase from './dataLayer';

const log = debug('fcc:handler');

export const graphqlSchema = makeExecutableSchema({
typeDefs,
resolvers,
directiveResolvers: createDirectives(),
logger: console
});

// Database connection logic lives outside of the handler for performance reasons
const connectToDatabase = require('./dataLayer');

const server = require('apollo-server-lambda');

exports.graphqlHandler = function graphqlHandler(event, context, callback) {
exports.graphqlHandler = async function graphqlHandler(
event,
context,
callback
) {
/* Cause Lambda to freeze the process and save state data after
the callback is called the effect is that new handler invocations
will be able to re-use the database connection.
See https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
and https://www.mongodb.com/blog/post/optimizing-aws-lambda-performance-with-mongodb-atlas-and-nodejs */
the callback is called. the effect is that new handler invocations
will be able to re-use the database connection.
See https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
and https://www.mongodb.com/blog/post/optimizing-aws-lambda-performance-with-mongodb-atlas-and-nodejs */
context.callbackWaitsForEmptyEventLoop = false;

function callbackFilter(error, output) {
Expand All @@ -35,17 +40,31 @@ exports.graphqlHandler = function graphqlHandler(event, context, callback) {
callback(error, output);
}

const handler = server.graphqlLambda({ schema: graphqlSchema });

connectToDatabase()
.then(() => {
return handler(event, context, callbackFilter);
})
.catch(err => {
console.log('MongoDB connection error: ', err);
// TODO: return 500?
process.exit();
});
const handler = graphqlLambda((event, context) => {
const { headers } = event;
const { functionName } = context;

return {
schema: graphqlSchema,
context: {
headers,
functionName,
event,
context
}
};
});

try {
await connectToDatabase();
} catch (err) {
log('MongoDB connection error: ', err);
// TODO: return 500?
/* eslint-disable no-process-exit */
process.exit();
/* eslint-enable no-process-exit */
}
return handler(event, context, callbackFilter);
};

exports.apiHandler = lambdaPlayground({
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
globalSetup: './test/utils/setup.js',
globalTeardown: './test/utils/teardown.js',
testEnvironment: './test/utils/mongo-environment.js'
testEnvironment: './test/utils/test-environment.js'
};
Loading