WIP: Successful Google Auth, broke pretty much everything else in the process
This commit is contained in:
@@ -4,7 +4,7 @@ import { type Context } from 'koa';
|
||||
export default function apiWrapper() {
|
||||
return async function apiWrapperMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<void>
|
||||
next: () => Promise<*>
|
||||
) {
|
||||
await next();
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
// @flow
|
||||
import JWT from 'jsonwebtoken';
|
||||
import { type Context } from 'koa';
|
||||
import { User, ApiKey } from '../../models';
|
||||
import { AuthenticationError, UserSuspendedError } from '../../errors';
|
||||
|
||||
export default function auth(options?: { required?: boolean } = {}) {
|
||||
return async function authMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<void>
|
||||
) {
|
||||
let token;
|
||||
|
||||
const authorizationHeader = ctx.request.get('authorization');
|
||||
if (authorizationHeader) {
|
||||
const parts = authorizationHeader.split(' ');
|
||||
if (parts.length === 2) {
|
||||
const scheme = parts[0];
|
||||
const credentials = parts[1];
|
||||
|
||||
if (/^Bearer$/i.test(scheme)) {
|
||||
token = credentials;
|
||||
}
|
||||
} else {
|
||||
throw new AuthenticationError(
|
||||
`Bad Authorization header format. Format is "Authorization: Bearer <token>"`
|
||||
);
|
||||
}
|
||||
// $FlowFixMe
|
||||
} else if (ctx.body.token) {
|
||||
token = ctx.body.token;
|
||||
} else if (ctx.request.query.token) {
|
||||
token = ctx.request.query.token;
|
||||
}
|
||||
|
||||
if (!token && options.required !== false) {
|
||||
throw new AuthenticationError('Authentication required');
|
||||
}
|
||||
|
||||
let user;
|
||||
if (token) {
|
||||
if (String(token).match(/^[\w]{38}$/)) {
|
||||
// API key
|
||||
let apiKey;
|
||||
try {
|
||||
apiKey = await ApiKey.findOne({
|
||||
where: {
|
||||
secret: token,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
throw new AuthenticationError('Invalid API key');
|
||||
}
|
||||
|
||||
if (!apiKey) throw new AuthenticationError('Invalid API key');
|
||||
|
||||
user = await User.findById(apiKey.userId);
|
||||
if (!user) throw new AuthenticationError('Invalid API key');
|
||||
} else {
|
||||
// JWT
|
||||
// Get user without verifying payload signature
|
||||
let payload;
|
||||
try {
|
||||
payload = JWT.decode(token);
|
||||
} catch (e) {
|
||||
throw new AuthenticationError('Unable to decode JWT token');
|
||||
}
|
||||
|
||||
if (!payload) throw new AuthenticationError('Invalid token');
|
||||
|
||||
user = await User.findById(payload.id);
|
||||
|
||||
try {
|
||||
JWT.verify(token, user.jwtSecret);
|
||||
} catch (e) {
|
||||
throw new AuthenticationError('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
const suspendingAdmin = await User.findById(user.suspendedById);
|
||||
throw new UserSuspendedError({ adminEmail: suspendingAdmin.email });
|
||||
}
|
||||
|
||||
ctx.state.token = token;
|
||||
ctx.state.user = user;
|
||||
// $FlowFixMe
|
||||
ctx.cache[user.id] = user;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
// Export JWT methods as a convenience
|
||||
export const sign = JWT.sign;
|
||||
export const verify = JWT.verify;
|
||||
export const decode = JWT.decode;
|
||||
@@ -1,187 +0,0 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb, seed } from '../../test/support';
|
||||
import { buildUser } from '../../test/factories';
|
||||
import { ApiKey } from '../../models';
|
||||
import randomstring from 'randomstring';
|
||||
import auth from './authentication';
|
||||
|
||||
beforeEach(flushdb);
|
||||
|
||||
describe('Authentication middleware', async () => {
|
||||
describe('with JWT', () => {
|
||||
it('should authenticate with correct token', async () => {
|
||||
const state = {};
|
||||
const { user } = await seed();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||
},
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it('should return error with invalid token', async () => {
|
||||
const state = {};
|
||||
const { user } = await seed();
|
||||
const authMiddleware = auth();
|
||||
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}error`),
|
||||
},
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Invalid token');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('with API key', () => {
|
||||
it('should authenticate user with valid API key', async () => {
|
||||
const state = {};
|
||||
const { user } = await seed();
|
||||
const authMiddleware = auth();
|
||||
const key = await ApiKey.create({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${key.secret}`),
|
||||
},
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it('should return error with invalid API key', async () => {
|
||||
const state = {};
|
||||
const authMiddleware = auth();
|
||||
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${randomstring.generate(38)}`),
|
||||
},
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Invalid API key');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error message if no auth token is available', async () => {
|
||||
const state = {};
|
||||
const authMiddleware = auth();
|
||||
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
get: jest.fn(() => 'error'),
|
||||
},
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'Bad Authorization header format. Format is "Authorization: Bearer <token>"'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow passing auth token as a GET param', async () => {
|
||||
const state = {};
|
||||
const { user } = await seed();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
get: jest.fn(() => null),
|
||||
query: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
},
|
||||
body: {},
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it('should allow passing auth token in body params', async () => {
|
||||
const state = {};
|
||||
const { user } = await seed();
|
||||
const authMiddleware = auth();
|
||||
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
get: jest.fn(() => null),
|
||||
},
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
},
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
expect(state.user.id).toEqual(user.id);
|
||||
});
|
||||
|
||||
it('should return an error for suspended users', async () => {
|
||||
const state = {};
|
||||
const admin = await buildUser({});
|
||||
const user = await buildUser({
|
||||
suspendedAt: new Date(),
|
||||
suspendedById: admin.id,
|
||||
});
|
||||
const authMiddleware = auth();
|
||||
|
||||
try {
|
||||
await authMiddleware(
|
||||
{
|
||||
request: {
|
||||
get: jest.fn(() => `Bearer ${user.getJwtToken()}`),
|
||||
},
|
||||
state,
|
||||
cache: {},
|
||||
},
|
||||
jest.fn()
|
||||
);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual(
|
||||
'Your access has been suspended by the team admin'
|
||||
);
|
||||
expect(e.errorData.adminEmail).toEqual(admin.email);
|
||||
}
|
||||
});
|
||||
});
|
||||
26
server/api/middlewares/cache.js
Normal file
26
server/api/middlewares/cache.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
import debug from 'debug';
|
||||
import { type Context } from 'koa';
|
||||
|
||||
const debugCache = debug('cache');
|
||||
|
||||
export default function cache() {
|
||||
return async function cacheMiddleware(ctx: Context, next: () => Promise<*>) {
|
||||
ctx.cache = {};
|
||||
|
||||
ctx.cache.set = async (id, value) => {
|
||||
ctx.cache[id] = value;
|
||||
};
|
||||
|
||||
ctx.cache.get = async (id, def) => {
|
||||
if (ctx.cache[id]) {
|
||||
debugCache(`hit: ${id}`);
|
||||
} else {
|
||||
debugCache(`miss: ${id}`);
|
||||
ctx.cache.set(id, await def());
|
||||
}
|
||||
return ctx.cache[id];
|
||||
};
|
||||
return next();
|
||||
};
|
||||
}
|
||||
46
server/api/middlewares/errorHandling.js
Normal file
46
server/api/middlewares/errorHandling.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// @flow
|
||||
import Sequelize from 'sequelize';
|
||||
import { snakeCase } from 'lodash';
|
||||
import { type Context } from 'koa';
|
||||
|
||||
export default function errorHandling() {
|
||||
return async function errorHandlingMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
) {
|
||||
try {
|
||||
await next();
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500;
|
||||
let message = err.message || err.name;
|
||||
let error;
|
||||
|
||||
if (err instanceof Sequelize.ValidationError) {
|
||||
// super basic form error handling
|
||||
ctx.status = 400;
|
||||
if (err.errors && err.errors[0]) {
|
||||
message = `${err.errors[0].message} (${err.errors[0].path})`;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.match('Authorization error')) {
|
||||
ctx.status = 403;
|
||||
error = 'authorization_error';
|
||||
}
|
||||
|
||||
if (ctx.status === 500) {
|
||||
message = 'Internal Server Error';
|
||||
error = 'internal_server_error';
|
||||
ctx.app.emit('error', err, ctx);
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
ok: false,
|
||||
error: snakeCase(err.id || error),
|
||||
status: err.status,
|
||||
message,
|
||||
data: err.errorData ? err.errorData : undefined,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
19
server/api/middlewares/methodOverride.js
Normal file
19
server/api/middlewares/methodOverride.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// @flow
|
||||
import queryString from 'query-string';
|
||||
import { type Context } from 'koa';
|
||||
|
||||
export default function methodOverride() {
|
||||
return async function methodOverrideMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<*>
|
||||
) {
|
||||
if (ctx.method === 'POST') {
|
||||
// $FlowFixMe
|
||||
ctx.body = ctx.request.body;
|
||||
} else if (ctx.method === 'GET') {
|
||||
ctx.method = 'POST'; // eslint-disable-line
|
||||
ctx.body = queryString.parse(ctx.querystring);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { type Context } from 'koa';
|
||||
export default function pagination(options?: Object) {
|
||||
return async function paginationMiddleware(
|
||||
ctx: Context,
|
||||
next: () => Promise<void>
|
||||
next: () => Promise<*>
|
||||
) {
|
||||
const opts = {
|
||||
defaultLimit: 15,
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// @flow
|
||||
import validator from 'validator';
|
||||
import { ParamRequiredError, ValidationError } from '../../errors';
|
||||
import { validateColorHex } from '../../../shared/utils/color';
|
||||
|
||||
export default function validation() {
|
||||
return function validationMiddleware(ctx: Object, next: Function) {
|
||||
ctx.assertPresent = (value, message) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
throw new ParamRequiredError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertNotEmpty = (value, message) => {
|
||||
if (value === '') {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertEmail = (value, message) => {
|
||||
if (!validator.isEmail(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertUuid = (value, message) => {
|
||||
if (!validator.isUUID(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertPositiveInteger = (value, message) => {
|
||||
if (!validator.isInt(value, { min: 0 })) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertHexColor = (value, message) => {
|
||||
if (!validateColorHex(value)) {
|
||||
throw new ValidationError(message);
|
||||
}
|
||||
};
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user