feat(middleware-jwt): verify payload
MODIFY
packages/middleware-jwt/src/jwt.factory.ts
packages/middleware-jwt/src/jwt.factory.ts
@@ -3,9 +3,12 @@ import * as jwt from 'jsonwebtoken';
export type Payload = string | object | Buffer;
export type IssuedAt = { iat: number };
+export type ExpiresAt = { exp: number };
export type GenerateOptions = jwt.SignOptions & { secret: jwt.Secret };
export type VerifyOptions = jwt.VerifyOptions & { secret: string | Buffer };
+type VerifyTokenHandler = <T extends object>(opts: VerifyOptions) => (token: string) => Observable<T & IssuedAt>;
+
export const generateExpirationInHours = (hours = 1) =>
Math.floor(Date.now() / 1000) + (60 * 60 * hours);
@@ -15,7 +18,7 @@ export const generateToken = ({ secret, ...opts }: GenerateOptions) => (payload:
export const verifyToken = <T extends object>({ secret, ...opts }: VerifyOptions) => (token: string) =>
jwt.verify(token, secret, opts) as T & IssuedAt;
-export const verifyToken$ = <T extends object>({ secret, ...opts }: VerifyOptions) => (token: string) =>
+export const verifyToken$: VerifyTokenHandler = <T extends object>({ secret, ...opts }) => token =>
new Observable<T & IssuedAt>(subscriber => {
jwt.verify(token, secret, opts, (error, payload) => {
if (error) {

MODIFY
packages/middleware-jwt/src/jwt.middleware.ts
packages/middleware-jwt/src/jwt.middleware.ts
-import { throwError, of } from 'rxjs';
+import { throwError, of, Observable } from 'rxjs';
import { map, flatMap, catchError, tap } from 'rxjs/operators';
import { HttpError, HttpStatus, Middleware, HttpRequest } from '@marblejs/core';
import { parseAuthorizationHeader } from './jwt.util';
@@ -8,11 +8,14 @@ export type AuthorizeMiddlewareConfig = VerifyOptions;
const assignPayloadToRequest = (req: HttpRequest) => (payload: object) => req.user = payload;
-export const authorize$ = (config: AuthorizeMiddlewareConfig): Middleware => req$ =>
+export const authorize$ =
+ (config: AuthorizeMiddlewareConfig) =>
+ (verifyPayload$: (payload: any) => Observable<object>): Middleware => req$ =>
req$.pipe(
flatMap(req => of(req).pipe(
map(parseAuthorizationHeader),
- flatMap(verifyToken$(config)),
+ flatMap(verifyToken$<object>(config)),
+ flatMap(verifyPayload$),
tap(assignPayloadToRequest(req)),
flatMap(() => req$),
)),

MODIFY
packages/middleware-jwt/src/spec/jwt.middleware.spec.ts
packages/middleware-jwt/src/spec/jwt.middleware.spec.ts
import { HttpRequest, HttpResponse, HttpError, HttpStatus } from '@marblejs/core';
-import { of, throwError } from 'rxjs';
import { authorize$ } from '@marblejs/middleware-jwt/src/jwt.middleware';
+import { of, throwError, iif } from 'rxjs';
+import { flatMap } from 'rxjs/operators';
+
+const verifyPayload$ = (payload: { id: string }) =>
+ of(payload).pipe(
+ flatMap(payload => iif(
+ () => payload.id !== 'test_id',
+ throwError(new Error()),
+ of(payload)
+ )),
+ );
describe('JWT middleware', () => {
let utilModule;
@@ -21,7 +31,7 @@ describe('JWT middleware', () => {
// given
const mockedSecret = 'test_secret';
const mockedToken = 'TEST_TOKEN';
- const mockedTokenPayload = { id: 'tes_id' };
+ const mockedTokenPayload = { id: 'test_id' };
const mockedRequest = { headers: { authorization: `Bearer ${mockedToken}`} } as HttpRequest;
const expectedRequest = { ...mockedRequest, user: mockedTokenPayload };
@@ -32,7 +42,7 @@ describe('JWT middleware', () => {
utilModule.parseAuthorizationHeader = jest.fn(() => mockedToken);
factoryModule.verifyToken$ = jest.fn(() => () => of(mockedTokenPayload));
- const middleware$ = authorize$({ secret: mockedSecret })(req$, res, undefined);
+ const middleware$ = authorize$({ secret: mockedSecret })(verifyPayload$)(req$, res, undefined);
// then
middleware$.subscribe(
@@ -63,7 +73,7 @@ describe('JWT middleware', () => {
utilModule.parseAuthorizationHeader = jest.fn(() => mockedToken);
factoryModule.verifyToken$ = jest.fn(() => () => throwError(expectedError));
- const middleware$ = authorize$({ secret: mockedSecret })(req$, res, undefined);
+ const middleware$ = authorize$({ secret: mockedSecret })(verifyPayload$)(req$, res, undefined);
// then
middleware$.subscribe(
@@ -79,4 +89,34 @@ describe('JWT middleware', () => {
}
);
});
+
+ test('authorize$ throws error if verifyPayload$ handler doesn\'t pass', done => {
+ // given
+ const mockedSecret = 'test_secret';
+ const mockedToken = 'TEST_TOKEN';
+ const mockedTokenPayload = { id: 'test_id_wrong' };
+ const mockedRequest = { headers: { authorization: `Bearer ${mockedToken}`} } as HttpRequest;
+ const expectedError = new HttpError('Unauthorized', HttpStatus.UNAUTHORIZED);
+
+ const req$ = of(mockedRequest);
+ const res = {} as HttpResponse;
+
+ // when
+ utilModule.parseAuthorizationHeader = jest.fn(() => mockedToken);
+ factoryModule.verifyToken$ = jest.fn(() => () => of(mockedTokenPayload));
+
+ const middleware$ = authorize$({ secret: mockedSecret })(verifyPayload$)(req$, res, undefined);
+
+ // then
+ middleware$.subscribe(
+ () => {
+ fail(`Stream should throw an error`);
+ done();
+ },
+ err => {
+ expect(err).toEqual(expectedError);
+ done();
+ }
+ );
+ });
});

MODIFY
packages/middleware-jwt/test/api.integration.spec.ts
packages/middleware-jwt/test/api.integration.spec.ts
@@ -39,7 +39,20 @@ describe('API integration', () => {
});
});
- test('GET /api/secured authorizes request and checks the `req.user` object', async () =>
+ test('GET /api/secured return 401 if payload doesn\'t pass verification', async () => {
+ const token = await request(app)
+ .post('/api/login')
+ .send({ ...LOGIN_CREDENTIALS, email: 'doesnt_exists@admin.com' })
+ .expect(200)
+ .then(req => req.body.token as string);
+
+ return request(app)
+ .get('/api/secured')
+ .set('Authorization', `Bearer ${token}`)
+ .expect(401, { error: { status: 401, message: 'Unauthorized' } });
+ });
+
+ test('GET /api/secured returns 401 if token is wrong', async () =>
request(app)
.get('/api/secured')
.set('Authorization', `Bearer WRONG_TOKEN`)

MODIFY
packages/middleware-jwt/test/api.integration.ts
packages/middleware-jwt/test/api.integration.ts
-import { authorize$, generateToken } from '../src';
+import { authorize$ as authorizeMiddleware$, generateToken } from '../src';
import { httpListener, EffectFactory, combineRoutes } from '@marblejs/core';
import { bodyParser$ } from '@marblejs/middleware-body';
-import { map, mapTo } from 'rxjs/operators';
+import { map, flatMap } from 'rxjs/operators';
+import { of, iif, throwError } from 'rxjs';
type LoginCredentials = { email: string, password: string };
+type Payload = { id: string, email: string};
export const SECRET_KEY = 'SOME_SSH_KEY';
-const generateFakeJWTPayload = ({ email }: LoginCredentials) => ({ id: 'test_id', email });
+const verifyPayload$ = (payload: { id: string, email: string}) =>
+ of(payload).pipe(
+ flatMap(payload => iif(
+ () => payload.id !== 'test_id' || payload.email !== 'admin@admin.com',
+ throwError(new Error()),
+ of(payload)
+ )),
+ );
+
+const authOptions = { secret: SECRET_KEY };
+const authorize$ = authorizeMiddleware$(authOptions)(verifyPayload$);
+const generateFakeJWTPayload = ({ email }: LoginCredentials): Payload => ({ id: 'test_id', email });
const login$ = EffectFactory
.matchPath('/login')
@@ -28,7 +41,7 @@ const securedRoot$ = EffectFactory
const secured$ = combineRoutes('/secured', {
effects: [securedRoot$],
- middlewares: [authorize$({ secret: SECRET_KEY })],
+ middlewares: [authorize$],
});
const api$ = combineRoutes('/api', [login$, secured$]);

feat(middleware-jwt): verify payload