Skip to content

Commit fae6e2c

Browse files
committed
Authentication Bearer addition
1 parent ed3e883 commit fae6e2c

File tree

10 files changed

+74
-98
lines changed

10 files changed

+74
-98
lines changed

src/auth/authUtils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ import { Types } from 'mongoose';
55
import User from '../database/model/User';
66
import { tokenInfo } from '../config';
77

8-
export const validateTokenData = async (payload: JwtPayload, userId: Types.ObjectId): Promise<JwtPayload> => {
8+
export const getAccessToken = (authorization: string) => {
9+
if (!authorization) throw new AuthFailureError('Invalid Authorization');
10+
if (!authorization.startsWith('Bearer ')) throw new AuthFailureError('Invalid Authorization');
11+
return authorization.split(' ')[1];
12+
};
13+
14+
export const validateTokenData = (payload: JwtPayload, userId: Types.ObjectId): boolean => {
915
if (!payload || !payload.iss || !payload.sub || !payload.aud || !payload.prm
1016
|| payload.iss !== tokenInfo.issuer
1117
|| payload.aud !== tokenInfo.audience
1218
|| payload.sub !== userId.toHexString())
1319
throw new AuthFailureError('Invalid Access Token');
14-
return payload;
20+
return true;
1521
};
1622

1723
export const createTokens = async (user: User, accessTokenKey: string, refreshTokenKey: string)

src/auth/authentication.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { AuthFailureError, AccessTokenError, TokenExpiredError } from '../core/A
55
import JWT, { ValidationParams } from '../core/JWT';
66
import KeystoreRepo from '../database/repository/KeystoreRepo';
77
import { Types } from 'mongoose';
8-
import { validateTokenData } from './authUtils';
8+
import { getAccessToken } from './authUtils';
99
import { tokenInfo } from '../config';
1010
import validator, { ValidationSource } from '../helpers/validator';
1111
import schema from './schema';
@@ -15,21 +15,24 @@ const router = express.Router();
1515

1616
export default router.use(validator(schema.auth, ValidationSource.HEADER),
1717
asyncHandler(async (req: ProtectedRequest, res, next) => {
18-
req.accessToken = req.headers['x-access-token'].toString();
19-
20-
const user = await UserRepo.findById(new Types.ObjectId(req.headers['x-user-id'].toString()));
21-
if (!user) throw new AuthFailureError('User not registered');
22-
req.user = user;
18+
req.accessToken = getAccessToken(req.headers.authorization); // Express headers are auto converted to lowercase
2319

2420
try {
21+
const jwtPayload = await JWT.decode(req.accessToken);
22+
if (!jwtPayload.sub || !Types.ObjectId.isValid(jwtPayload.sub))
23+
throw new AuthFailureError('Invalid access token');
24+
25+
const user = await UserRepo.findById(new Types.ObjectId(jwtPayload.sub));
26+
if (!user) throw new AuthFailureError('User not registered');
27+
req.user = user;
28+
2529
const payload = await JWT.validate(
2630
req.accessToken,
27-
new ValidationParams(tokenInfo.issuer, tokenInfo.audience, user._id.toHexString()));
31+
new ValidationParams(tokenInfo.issuer, tokenInfo.audience, req.user._id.toHexString()));
2832

29-
const jwtPayload = await validateTokenData(payload, req.user._id);
3033
const keystore = await KeystoreRepo.findforKey(req.user._id, payload.prm);
3134

32-
if (!keystore || keystore.primaryKey !== jwtPayload.prm)
35+
if (!keystore || keystore.primaryKey !== payload.prm)
3336
throw new AuthFailureError('Invalid access token');
3437

3538
req.keystore = keystore;

src/auth/schema.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import Joi from '@hapi/joi';
2-
import { JoiObjectId } from '../helpers/validator';
2+
import { JoiAuthBearer } from '../helpers/validator';
33

44
export default {
55
apiKey: Joi.object().keys({
66
'x-api-key': Joi.string().required()
77
}).unknown(true),
88
auth: Joi.object().keys({
9-
'x-access-token': Joi.string().required(),
10-
'x-user-id': JoiObjectId().required(),
9+
'authorization': JoiAuthBearer().required(),
1110
}).unknown(true)
1211
};

src/core/JWT.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,33 +42,20 @@ export default class JWT {
4242
} catch (e) {
4343
Logger.debug(e);
4444
if (e && e.name === 'TokenExpiredError') throw new TokenExpiredError();
45+
// throws error if the token has not been encrypted by the private key
4546
throw new BadTokenError();
4647
}
4748
}
4849

4950
/**
50-
* This method checks the token and returns the decoded data even when the token is expired
51+
* Returns the decoded payload without verifying if the signature is valid.
5152
*/
52-
public static async decode(token: string, validations: ValidationParams): Promise<JwtPayload> {
53-
const cert = await this.readPublicKey();
53+
public static async decode(token: string): Promise<JwtPayload> {
5454
try {
55-
// token is verified if it was encrypted by the private key
56-
// and if is still not expired then get the payload
57-
// @ts-ignore
58-
return <JwtPayload>await promisify(verify)(token, cert, validations);
55+
return <JwtPayload>decode(token);
5956
} catch (e) {
6057
Logger.debug(e);
61-
if (e && e.name === 'TokenExpiredError') {
62-
// if the token has expired but was encryped by the private key
63-
// then decode it to get the payload
64-
// @ts-ignore
65-
return <JwtPayload>decode(token);
66-
}
67-
else {
68-
// throws error if the token has not been encrypted by the private key
69-
// or has not been issued for the user
70-
throw new BadTokenError();
71-
}
58+
throw new BadTokenError();
7259
}
7360
}
7461
}

src/helpers/validator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export const JoiUrlEndpoint = () => Joi.string().custom((value: string, helpers)
2121
return value;
2222
}, 'Url Endpoint Validation');
2323

24+
export const JoiAuthBearer = () => Joi.string().custom((value: string, helpers) => {
25+
if (!value.startsWith('Bearer ')) return helpers.error('any.invalid');
26+
if (!value.split(' ')[1]) return helpers.error('any.invalid');
27+
return value;
28+
}, 'Authorization Header Validation');
2429

2530
export default (schema: Joi.ObjectSchema, source: ValidationSource = ValidationSource.BODY) =>
2631
(req: Request, res: Response, next: NextFunction) => {

src/routes/v1/access/token.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { AuthFailureError, } from '../../../core/ApiError';
77
import JWT, { ValidationParams } from '../../../core/JWT';
88
import KeystoreRepo from '../../../database/repository/KeystoreRepo';
99
import crypto from 'crypto';
10-
import { validateTokenData, createTokens } from '../../../auth/authUtils';
10+
import { validateTokenData, createTokens, getAccessToken } from '../../../auth/authUtils';
1111
import validator, { ValidationSource } from '../../../helpers/validator';
1212
import schema from './schema';
1313
import asyncHandler from '../../../helpers/asyncHandler';
@@ -18,29 +18,23 @@ const router = express.Router();
1818
router.post('/refresh',
1919
validator(schema.auth, ValidationSource.HEADER), validator(schema.refreshToken),
2020
asyncHandler(async (req: ProtectedRequest, res, next) => {
21-
req.accessToken = req.headers['x-access-token'].toString();
21+
req.accessToken = getAccessToken(req.headers.authorization); // Express headers are auto converted to lowercase
2222

23-
const user = await UserRepo.findById(new Types.ObjectId(req.headers['x-user-id'].toString()));
23+
const accessTokenPayload = await JWT.decode(req.accessToken);
24+
if (!accessTokenPayload.sub || !Types.ObjectId.isValid(accessTokenPayload.sub))
25+
throw new AuthFailureError('Invalid access token');
26+
27+
const user = await UserRepo.findById(new Types.ObjectId(accessTokenPayload.sub));
2428
if (!user) throw new AuthFailureError('User not registered');
2529
req.user = user;
2630

27-
const accessTokenPayload = await validateTokenData(
28-
await JWT.decode(req.accessToken,
29-
new ValidationParams(
30-
tokenInfo.issuer,
31-
tokenInfo.audience,
32-
req.user._id.toHexString())),
33-
req.user._id
34-
);
31+
validateTokenData(accessTokenPayload, req.user._id);
3532

36-
const refreshTokenPayload = await validateTokenData(
37-
await JWT.validate(req.body.refreshToken,
38-
new ValidationParams(
39-
tokenInfo.issuer,
40-
tokenInfo.audience,
41-
req.user._id.toHexString())),
42-
req.user._id
43-
);
33+
const refreshTokenPayload = await JWT.validate(req.body.refreshToken,
34+
new ValidationParams(
35+
tokenInfo.issuer,
36+
tokenInfo.audience,
37+
req.user._id.toHexString()));
4438

4539
const keystore = await KeystoreRepo.find(
4640
req.user._id,

tests/auth/authentication/mock.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export const mockUserFindById = jest.fn(async (id: Types.ObjectId) => {
1616
else return null;
1717
});
1818

19+
export const mockJwtDecode = jest.fn(async (token: string): Promise<JwtPayload> => {
20+
if (token == ACCESS_TOKEN) return <JwtPayload>{ sub: USER_ID.toHexString() };
21+
throw new BadTokenError();
22+
});
23+
1924
export const mockJwtValidate = jest.fn(
2025
async (token: string, validations: ValidationParams): Promise<JwtPayload> => {
2126
if (token == ACCESS_TOKEN) return <JwtPayload>{ prm: 'abcdef' };
@@ -25,13 +30,6 @@ export const mockJwtValidate = jest.fn(
2530
export const mockKeystoreFindForKey = jest.fn(
2631
async (client: User, key: string): Promise<Keystore> => (<Keystore>{ client: client, primaryKey: key }));
2732

28-
export const mockValidateTokenData =
29-
jest.fn(async (payload: JwtPayload, userId: Types.ObjectId): Promise<JwtPayload> => payload);
30-
31-
jest.mock('../../../src/auth/authUtils', () => ({
32-
get validateTokenData() { return mockValidateTokenData; }
33-
}));
34-
3533
jest.mock('../../../src/database/repository/UserRepo', () => ({
3634
get findById() { return mockUserFindById; }
3735
}));
@@ -41,13 +39,13 @@ jest.mock('../../../src/database/repository/KeystoreRepo', () => ({
4139
}));
4240

4341
JWT.validate = mockJwtValidate;
42+
JWT.decode = mockJwtDecode;
4443

4544
export const addHeaders = (request: any) => request
4645
.set('Content-Type', 'application/json')
4746
.set('x-api-key', API_KEY);
4847

49-
export const addAuthHeaders = (request: any, userId: Types.ObjectId = USER_ID) => request
48+
export const addAuthHeaders = (request: any) => request
5049
.set('Content-Type', 'application/json')
51-
.set('x-api-key', API_KEY)
52-
.set('x-access-token', ACCESS_TOKEN)
53-
.set('x-user-id', userId.toHexString());
50+
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
51+
.set('x-api-key', API_KEY);
Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
USER_ID, ACCESS_TOKEN, addHeaders, addAuthHeaders,
3-
mockValidateTokenData, mockUserFindById, mockJwtValidate, mockKeystoreFindForKey
3+
mockUserFindById, mockJwtValidate, mockJwtDecode, mockKeystoreFindForKey
44
} from './mock';
55

66
import app from '../../../src/app';
@@ -12,63 +12,48 @@ describe('authentication validation', () => {
1212
const request = supertest(app);
1313

1414
beforeEach(() => {
15-
mockValidateTokenData.mockClear();
1615
mockUserFindById.mockClear();
1716
mockJwtValidate.mockClear();
17+
mockJwtDecode.mockClear();
1818
mockKeystoreFindForKey.mockClear();
1919
});
2020

21-
it('Should response with 400 if x-access-token header is not passed', async () => {
22-
const response = await addHeaders(request.get(endpoint))
23-
.set('x-user-id', USER_ID.toHexString());
21+
it('Should response with 400 if Authorization header is not passed', async () => {
22+
const response = await addHeaders(request.get(endpoint));
2423
expect(response.status).toBe(400);
25-
expect(response.body.message).toMatch(/x-access-token/);
24+
expect(response.body.message).toMatch(/authorization/);
25+
expect(mockJwtDecode).not.toBeCalled();
2626
expect(mockUserFindById).not.toBeCalled();
2727
});
2828

29-
it('Should response with 400 if x-user-id header is not passed', async () => {
30-
const response = await addHeaders(request.get(endpoint))
31-
.set('x-access-token', ACCESS_TOKEN);
32-
expect(response.status).toBe(400);
33-
expect(response.body.message).toMatch(/x-user-id/);
34-
expect(mockUserFindById).not.toBeCalled();
35-
});
3629

37-
it('Should response with 400 if x-user-id header is not mongoose id', async () => {
30+
it('Should response with 400 if Authorization header do not have Bearer', async () => {
3831
const response = await addHeaders(request.get(endpoint))
39-
.set('x-access-token', ACCESS_TOKEN)
40-
.set('x-user-id', '123');
32+
.set('Authorization', '123');
4133
expect(response.status).toBe(400);
42-
expect(response.body.message).toMatch(/x-user-id/);
34+
expect(response.body.message).toMatch(/authorization/);
35+
expect(mockJwtDecode).not.toBeCalled();
4336
expect(mockUserFindById).not.toBeCalled();
4437
});
4538

46-
it('Should response with 401 if wrong x-user-id header is provided', async () => {
39+
it('Should response with 401 if wrong Authorization header is provided', async () => {
4740
const response = await addHeaders(request.get(endpoint))
48-
.set('x-access-token', ACCESS_TOKEN)
49-
.set('x-user-id', '5e7b8c22d347fc2407c564a6'); // some random mongoose id
50-
expect(response.status).toBe(401);
51-
expect(response.body.message).toMatch(/not registered/);
52-
expect(mockUserFindById).toBeCalledTimes(1);
53-
});
54-
55-
it('Should response with 401 if wrong x-access-token header is provided', async () => {
56-
const response = await addHeaders(request.get(endpoint))
57-
.set('x-access-token', '123')
58-
.set('x-user-id', USER_ID);
41+
.set('Authorization', 'Bearer 123');
5942
expect(response.status).toBe(401);
6043
expect(response.body.message).toMatch(/token/i);
61-
expect(mockUserFindById).toBeCalledTimes(1);
62-
expect(mockJwtValidate).toBeCalledTimes(1);
44+
expect(mockJwtDecode).toBeCalledTimes(1);
45+
expect(mockJwtDecode).toBeCalledWith('123');
46+
expect(mockUserFindById).not.toBeCalled();
6347
});
6448

65-
it('Should response with 404 if correct x-access-token and x-user-id header are provided', async () => {
49+
it('Should response with 404 if correct Authorization header is provided', async () => {
6650
const response = await addAuthHeaders(request.get(endpoint));
6751
expect(response.body.message).not.toMatch(/not registered/);
6852
expect(response.body.message).not.toMatch(/token/i);
6953
expect(response.status).toBe(404);
54+
expect(mockJwtDecode).toBeCalledTimes(1);
55+
expect(mockJwtDecode).toBeCalledWith(ACCESS_TOKEN);
7056
expect(mockUserFindById).toBeCalledTimes(1);
71-
expect(mockValidateTokenData).toBeCalledTimes(1);
7257
expect(mockJwtValidate).toBeCalledTimes(1);
7358
});
7459
});

tests/auth/authorization/mock.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Types } from 'mongoose';
55
import User from '../../../src/database/model/User';
66
import Role, { RoleCode } from '../../../src/database/model/Role';
77

8-
98
export const LEARNER_ROLE_ID = new Types.ObjectId(); // random id
109
export const WRITER_ROLE_ID = new Types.ObjectId(); // random id
1110
export const EDITOR_ROLE_ID = new Types.ObjectId(); // random id

tests/auth/authorization/unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { addAuthHeaders } from '../authentication/mock';
22

33
// import the mock for the current test after all other mock imports
44
// this will prevent the different implementations by the other mock
5-
import { mockRoleRepoFindByCode, mockUserFindById, USER_ID_WRITER } from './mock';
5+
import { mockRoleRepoFindByCode, mockUserFindById } from './mock';
66

77
import app from '../../../src/app';
88
import supertest from 'supertest';
@@ -39,7 +39,7 @@ describe('authentication validation for writer', () => {
3939
});
4040

4141
it('Should response with 404 if user have writer role', async () => {
42-
const response = await addAuthHeaders(request.get(endpoint), USER_ID_WRITER);
42+
const response = await addAuthHeaders(request.get(endpoint));
4343
expect(response.status).toBe(404);
4444
expect(mockRoleRepoFindByCode).toBeCalledTimes(1);
4545
expect(mockUserFindById).toBeCalledTimes(1);

0 commit comments

Comments
 (0)