diff --git a/api/generate/openapi.test.json b/api/generate/openapi.test.json index a33e9c998..75bb5a556 100644 --- a/api/generate/openapi.test.json +++ b/api/generate/openapi.test.json @@ -92,18 +92,25 @@ "Test" ], "summary": "Authorization header required", - "security": [ - { - "jwt": [] - } - ], "parameters": [], "responses": { "200": { "description": "", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/AuthRequiredResponse" + } + } + } + }, + "default": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HttpErrorResponse" + } } } } @@ -143,6 +150,33 @@ "additionalProperties": { "type": "string" } + }, + "AuthRequiredResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "message" + ] + }, + "HttpErrorResponse": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "messages" + ] } }, "securitySchemes": { diff --git a/api/package-lock.json b/api/package-lock.json index 8995933be..379aecbfd 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -13,6 +13,7 @@ "@prisma/client": "^5.5.2", "axios": "^1.6.3", "config": "^3.3.9", + "cookie-parser": "^1.4.6", "crockford-base32": "^2.0.0", "express": "^4.18.2", "express-async-errors": "^3.1.1", @@ -34,6 +35,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/config": "^3.3.0", "@types/convict-format-with-validator": "^6.0.2", + "@types/cookie-parser": "^1.4.6", "@types/express": "^4.17.17", "@types/express-serve-static-core": "^4.17.35", "@types/jest": "^29.5.1", @@ -2147,6 +2149,15 @@ "@types/convict": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", @@ -3553,6 +3564,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/api/package.json b/api/package.json index 098e0448e..b64ad23ae 100644 --- a/api/package.json +++ b/api/package.json @@ -30,6 +30,7 @@ "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/config": "^3.3.0", "@types/convict-format-with-validator": "^6.0.2", + "@types/cookie-parser": "^1.4.6", "@types/express": "^4.17.17", "@types/express-serve-static-core": "^4.17.35", "@types/jest": "^29.5.1", @@ -55,6 +56,7 @@ "@prisma/client": "^5.5.2", "axios": "^1.6.3", "config": "^3.3.9", + "cookie-parser": "^1.4.6", "crockford-base32": "^2.0.0", "express": "^4.18.2", "express-async-errors": "^3.1.1", diff --git a/api/src/config/loader.ts b/api/src/config/loader.ts index 6972f9041..c76ab2673 100644 --- a/api/src/config/loader.ts +++ b/api/src/config/loader.ts @@ -51,7 +51,8 @@ export function buildHttpConfig(config: Config): HttpConfig { return { env: config.env, host: config.http.host, - port: config.http.port + port: config.http.port, + cookieExpirationHours: config.jwt.expirationHour }; } diff --git a/api/src/controller/http/middleware.ts b/api/src/controller/http/middleware.ts index 676e5ac1b..4a21fad48 100644 --- a/api/src/controller/http/middleware.ts +++ b/api/src/controller/http/middleware.ts @@ -3,12 +3,17 @@ import 'express-async-errors'; import { HttpError as ValidationError } from 'express-openapi-validator/dist/framework/types'; import { Logger } from 'winston'; -import { CustomError, HttpErrorCode } from '@src/error/errors'; +import { JwtHandler } from '@src/core/ports/jwt.handler'; +import { AppErrorCode, CustomError, HttpErrorCode } from '@src/error/errors'; import { HttpErrorResponse } from '@controller/http/response'; +import { idTokenCookieName } from '@controller/http/types'; export class Middleware { - constructor(public logger: Logger) {} + constructor( + private readonly logger: Logger, + private readonly jwtHandler: JwtHandler + ) {} public accessLog = (req: Request, res: Response, next: NextFunction) => { const start = new Date().getTime(); @@ -33,7 +38,7 @@ export class Middleware { }; public handleError = ( - err: Error, + err: unknown, req: Request, res: Response, next: NextFunction @@ -70,6 +75,17 @@ export class Middleware { ); }; + public issuePassport = (req: Request, res: Response, next: NextFunction) => { + let passport = this.issuePassportFromHeader(req); + + if (passport === null) { + passport = this.issuePassportFromCookie(req); + } + + res.locals.passport = passport; + next(); + }; + private convertValidationErrorToCustomError = ( err: ValidationError ): CustomError => { @@ -171,4 +187,35 @@ export class Middleware { messages: err.messages }); }; + + private issuePassportFromHeader = (req: Request) => { + const authHeader = req.get('Authorization'); + if (!authHeader) { + return null; + } + + const splittedAuthHeader = authHeader.split(' ', 2); + if (splittedAuthHeader.length !== 2 || splittedAuthHeader[0] !== 'Bearer') { + throw new CustomError({ + code: AppErrorCode.UNAUTHENTICATED, + message: 'Authorization header must be in Bearer format', + context: { splittedAuthHeader } + }); + } + + return this.jwtHandler.verifyAppIdToken(splittedAuthHeader[1]); + }; + + private issuePassportFromCookie = (req: Request) => { + const cookie = req.cookies[idTokenCookieName]; + if (!cookie) { + throw new CustomError({ + code: AppErrorCode.UNAUTHENTICATED, + message: 'cookie does not exist', + context: { cookie } + }); + } + + return this.jwtHandler.verifyAppIdToken(cookie); + }; } diff --git a/api/src/controller/http/server.ts b/api/src/controller/http/server.ts index d80e1ce6c..a0c457baf 100644 --- a/api/src/controller/http/server.ts +++ b/api/src/controller/http/server.ts @@ -1,3 +1,4 @@ +import cookieParser from 'cookie-parser'; import express from 'express'; import { middleware as OpenApiValidatorMiddleware } from 'express-openapi-validator'; import { Express } from 'express-serve-static-core'; @@ -10,6 +11,7 @@ import { Logger } from 'winston'; import apiSpecification from '@root/generate/openapi.json'; import { name, version } from '@root/package.json'; +import { JwtHandler } from '@src/core/ports/jwt.handler'; import { AuthService } from '@src/core/services/auth/auth.service'; import { AuthV1Controller } from '@controller/http/auth/auth.v1.controller'; @@ -26,9 +28,10 @@ export class HttpServer { constructor( private readonly logger: Logger, private readonly config: HttpConfig, - private readonly authService: AuthService + private readonly authService: AuthService, + private readonly jwtHandler: JwtHandler ) { - this.middleware = new Middleware(this.logger); + this.middleware = new Middleware(this.logger, this.jwtHandler); } public start = async (): Promise => { @@ -37,7 +40,9 @@ export class HttpServer { this.app.set('trust proxy', 0); this.app.use(express.json()); await this.buildApiDocument(); + this.app.use('/api', cookieParser()); this.app.use('/api', this.middleware.accessLog); + this.app.use('/api/v1/dev', this.middleware.issuePassport); this.app.use( OpenApiValidatorMiddleware({ apiSpec: path.join(__dirname, '../../../generate/openapi.json'), @@ -71,7 +76,10 @@ export class HttpServer { private getApiRouters = (): express.Router[] => { const routers = [ new DevV1Controller().routes(), - new AuthV1Controller(this.authService).routes() + new AuthV1Controller( + this.authService, + this.config.cookieExpirationHours + ).routes() ]; return routers; }; diff --git a/api/src/controller/http/types.ts b/api/src/controller/http/types.ts index 7b5970882..d76a3b172 100644 --- a/api/src/controller/http/types.ts +++ b/api/src/controller/http/types.ts @@ -1,5 +1,8 @@ +export const idTokenCookieName = 'mrcToken'; + export interface HttpConfig { env: string; host: string; port: number; + cookieExpirationHours: number; } diff --git a/api/test/config/loader.test.ts b/api/test/config/loader.test.ts index 707892ad7..bba3d8d48 100644 --- a/api/test/config/loader.test.ts +++ b/api/test/config/loader.test.ts @@ -90,7 +90,8 @@ describe('Test build http config', () => { expect(buildHttpConfig(loadConfig())).toStrictEqual({ env: 'test', host: '127.0.0.1', - port: 0 + port: 0, + cookieExpirationHours: 1 }); }); }); diff --git a/api/test/controller/http/handler.test.ts b/api/test/controller/http/handler.test.ts index 9de05e305..11f04f92f 100644 --- a/api/test/controller/http/handler.test.ts +++ b/api/test/controller/http/handler.test.ts @@ -7,6 +7,7 @@ import { Logger } from 'winston'; import { methodNotAllowed } from '@src/controller/http/handler'; import { Middleware } from '@src/controller/http/middleware'; +import { JwtHandler } from '@src/core/ports/jwt.handler'; // TODO: https://github.com/MovieReviewComment/Mr.C/issues/49 class TestHttpServer { @@ -14,8 +15,11 @@ class TestHttpServer { server!: http.Server; app!: Express; - constructor(private readonly logger: Logger) { - this.middleware = new Middleware(this.logger); + constructor( + private readonly logger: Logger, + private readonly jwtHandler: JwtHandler + ) { + this.middleware = new Middleware(this.logger, this.jwtHandler); } public start = (): Promise => { @@ -70,12 +74,17 @@ class TestController { describe('Test handler', () => { let mockLogger: Partial; + let mockJwtHandler: Partial; let testHttpServer: TestHttpServer; let baseUrl: string; beforeAll(async () => { mockLogger = {}; - testHttpServer = new TestHttpServer(mockLogger as Logger); + mockJwtHandler = {}; + testHttpServer = new TestHttpServer( + mockLogger as Logger, + mockJwtHandler as JwtHandler + ); baseUrl = '/api/v1/dev'; await testHttpServer.start(); }); diff --git a/api/test/controller/http/middleware.test.ts b/api/test/controller/http/middleware.test.ts index abaf1f425..2a3d90848 100644 --- a/api/test/controller/http/middleware.test.ts +++ b/api/test/controller/http/middleware.test.ts @@ -1,3 +1,4 @@ +import cookieParser from 'cookie-parser'; import { Request, Response, Router } from 'express'; import express from 'express'; import { middleware as OpenApiValidatorMiddleware } from 'express-openapi-validator'; @@ -9,15 +10,24 @@ import { Tspec, generateTspec } from 'tspec'; import { Logger } from 'winston'; import { Middleware } from '@src/controller/http/middleware'; +import { AppIdToken } from '@src/core/entities/auth.entity'; +import { JwtHandler } from '@src/core/ports/jwt.handler'; import { CustomError, HttpErrorCode } from '@src/error/errors'; +import { JwtClient } from '@src/jwt/jwt.client'; + +import { HttpErrorResponse } from '@controller/http/response'; +import { idTokenCookieName } from '@controller/http/types'; class TestHttpServer { middleware: Middleware; server!: http.Server; app!: Express; - constructor(private readonly logger: Logger) { - this.middleware = new Middleware(this.logger); + constructor( + private readonly logger: Logger, + private readonly jwtHandler: JwtHandler + ) { + this.middleware = new Middleware(this.logger, this.jwtHandler); } public start = async (): Promise => { @@ -31,6 +41,8 @@ class TestHttpServer { validateResponses: true }) ); + this.app.use('/api', cookieParser()); + this.app.use('/api/v1/dev/authRequired', this.middleware.issuePassport); this.app.use('/api', this.getRouters()); this.app.use(this.middleware.handleError); // TODO: https://github.com/MovieReviewComment/Mr.C/issues/49 @@ -82,6 +94,10 @@ class TestHttpServer { }; } +interface AuthRequiredResponse { + message: string; +} + type TestApiSpec = Tspec.DefineApiSpec<{ basePath: '/api/v1/dev'; tags: ['Test']; @@ -112,9 +128,11 @@ type TestApiSpec = Tspec.DefineApiSpec<{ }; '/authRequired': { get: { - security: 'jwt'; summary: 'Authorization header required'; - handler: typeof TestController.prototype.authRequired; + responses: { + 200: AuthRequiredResponse; + default: HttpErrorResponse; + }; }; }; }; @@ -128,6 +146,7 @@ class TestController { const prefix = '/v1/dev'; router.use(express.json()); + router.use(cookieParser()); router.post(`${prefix}/throwSyncCustomError`, this.throwSyncCustomError); router.post(`${prefix}/throwAsyncCustomError`, this.throwAsyncCustomError); router.post(`${prefix}/throwSyncError`, this.throwSyncError); @@ -160,19 +179,36 @@ class TestController { throw new Error('Error from throwAsyncError'); }; - public authRequired = async (req: Request, res: Response) => { + public authRequired = async ( + req: Request, + res: Response + ) => { res.send({ message: 'Authentication verified' }); }; } describe('Test middleware', () => { let mockLogger: Partial; + let jwtClient: JwtClient; let testHttpServer: TestHttpServer; let baseUrl: string; beforeAll(async () => { mockLogger = { error: jest.fn(), warn: jest.fn() }; - testHttpServer = new TestHttpServer(mockLogger as Logger); + jwtClient = new JwtClient({ + activeKeyPair: 'test', + expirationHour: 1, + keyPairs: [ + { + name: 'test', + private: + '-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAsNp22LRZ1VNL+0KI21Cd7XFIF5bdN4fiNjESGMcH8iylL0Pg\n924lYq/Jtt8wtBAVUn6W87hRqFN4jwrHfcaGd25aTf/MnR9asteSj8+d9K/s8UOL\nMQMRPjY0ud/NNL4Bn3LubjwLXjEkhV6bC9LeIZSAFiomjjqW2V4rFecffngdtOo8\nP77P19Rg5snQkLox1ABmVGE9+XWU2Hr1vZ+mh2WqwGmQr/j5Us6QZ8vvKR0oQdmz\nz9+P/1w/xwEYvZhZyn1hTWvBEufUQYWSb0ve/uKYA36hQt+ou2Y7ITRR0raFePtw\niwRfIT1cqeqG6+n+uMiT5kh70P6vQbglfCj91QIDAQABAoIBAGl4H+Bkzh42qt2R\ndGS20zhDkqbexdbUJsgCw7QbHlYC4hAp/wQQoCMWismQmU8JOG4WKJf4mFo2TXOh\nDg+oUZDwMtLJdpFNnZ2CillRi/Xc5QWNLnlwRtw/H3qqSYrmtbkNpbv/+xeVXx5a\nqUSH4QlNsoWFZbD0p/nB+xf42gNlSO5pOOqP5iXt5wJBbWhpomqFFZuD/+Pf6Uma\n+YNB6u6/obdS+AaHt3TIUck2CtMgZ49jI+rx87JEpNO2QVeiprTDBlZMyzxTQi3W\n3tfY5o5Nsu42ZyptZSTacNKgOzKub/7T0p6852j/fOXGQvVRT8baVFAeM9TrPExS\np2f/84ECgYEA3eaM/Q+flts3KJBpiTw5kwD5v6vAoZ4oGlJIxlMEjE4vLtruS1Fa\nYxChOeEmdu9oAk3drxDmtPmNTKXnlv75P2/h0Qi1burEF0wcQN5tUF1blcdgG4kr\nQVCf+Bwk03lVcPS8YxxeHCpq5NEEKLFImkjbY/X9pqWxQbsPjZOTfxkCgYEAzAfH\nznohrkWoy1guV/sxZjs6AIwBqM1uWhraXmDc+yVc1nNMKuyL5LKuaJFWSwXWMe2F\nhUT5BYs3ajtpEboxmQ+V9nCM6jbSSsxSrWe8sOffZj2hTbsJh8/N5rXTK1QaMRD3\ns1BQINAofxitFxKYWJ60tKAhTlmNMMwfH7Y7WB0CgYEAlN0cbJDUoWHDKUVoZ5at\nkT8wTTOt8T6m7LGS/OmovW+eG7Ln9kNHffokDy5KnbOSdSlDtTSDcZmQ/4C1UwkO\nsU4fkhpjjVuV3YND2QjfEPDwhhTRFuf4ysKJ7usCkZRui27EC0F2qTKTr5nBToNQ\nj6Cc/fyDBA9YUR5rGrGMW9ECgYEAxqKciAynVb9DwhSrqcRIJ7tpkLa9ttWppdeW\n2WN8QJXzeGTvtqps186Ntggo9wlLq3gPEdxAhIExBh+o/zVCrD1cRnz08+FDgsbB\nh0kDj0dvW16M99wsPyi00PQcDobmqPZX8R8zo36Erpgbi+byovSAAYoUYu8UYnmX\no4wK4pECgYEA2HY5ChDIAwucoN0mHDRsCUDQqopiI7rS9WckH6LnNbB5H9/mKvOK\ncHgqP1sH8xA9hYK1Gh5LljUHbwp3XAaLZULkDsMgPKG7DpnBN81Jq9avo0k9tl9b\nziVXu0bWgX8bIt9cmoG5eP9jx8oCTnR/l7xDxOTsDH1VV91lQjfj9fo=\n-----END RSA PRIVATE KEY-----\n', + public: + '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsNp22LRZ1VNL+0KI21Cd\n7XFIF5bdN4fiNjESGMcH8iylL0Pg924lYq/Jtt8wtBAVUn6W87hRqFN4jwrHfcaG\nd25aTf/MnR9asteSj8+d9K/s8UOLMQMRPjY0ud/NNL4Bn3LubjwLXjEkhV6bC9Le\nIZSAFiomjjqW2V4rFecffngdtOo8P77P19Rg5snQkLox1ABmVGE9+XWU2Hr1vZ+m\nh2WqwGmQr/j5Us6QZ8vvKR0oQdmzz9+P/1w/xwEYvZhZyn1hTWvBEufUQYWSb0ve\n/uKYA36hQt+ou2Y7ITRR0raFePtwiwRfIT1cqeqG6+n+uMiT5kh70P6vQbglfCj9\n1QIDAQAB\n-----END PUBLIC KEY-----\n' + } + ] + }); + testHttpServer = new TestHttpServer(mockLogger as Logger, jwtClient); baseUrl = '/api/v1/dev'; await testHttpServer.start(); }); @@ -228,19 +264,49 @@ describe('Test middleware', () => { expect(response.status).toEqual(400); }); - it('should 200 when correct authorization header passed', async () => { + it('should 200 when valid Authorization header is provided', async () => { const response = await request(testHttpServer.app) .get(`${baseUrl}/authRequired`) - .set('Authorization', 'Bearer test'); - expect(response.status).toEqual(200); + .set( + 'Authorization', + 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QifQ.eyJpc3MiOiJtb3ZpZS1yZWl2ZXctY29tbWVudCIsImlhdCI6MTcwNDAzNjQ5MywibmJmIjoxNzA0MDM2NDkzLCJleHAiOjE3MDY2Mjg0OTMsInVzZXJJZCI6IjU3Y2YyN2Q4LTBjNzQtNDk2My05NGE3LTQ3YzM1NzQxZWQwNiIsIm5pY2tuYW1lIjoi64-E7KCE7KCB7J24IO2PieuhoOqwgCDsl7DrkZDruZsg7ZWY66eIIiwidGFnIjoiI0FaN0oiLCJpZHAiOiJHT09HTEUiLCJlbWFpbCI6ImRvd2hhdHlvdXdhbnQwNzI0QGdtYWlsLmNvbSIsImFjY2Vzc0xldmVsIjoiVVNFUiJ9.NX10zbQRCNr-38tGeqV_nBx5VDqTC0uj3k15QuWQYbgBvZh1yIJp3PqmnEuXuuc8Jbm5HZC3p_exVopgAXLLfMqkleNOBh1KB46yQAeJEzl1H-LWPHGRHks_pGaSg0E2irqUCOsCnVAuzkYTefCnsQy7dXamsRr7g9pRppFyRyjzDWY1r5HsEiIw77GUNOZ31f87PkLTJaQHfBvXm5fyS541IIKXMkH_7DaoSjUBTPZOBpPGg3q9hjTbKn2mkESC8Y4E9KFlX9UwoTRtKFI8QaBPYPYvL4ZuKD1pJCEj7nbT0NPnRcC0M6eI0tIDHbTSbMDbSE-HZZrcwn9WYPVqNg' + ) + .expect(200); + expect(response.body.message).toEqual('Authentication verified'); }); - it('should 401 when authorization header does not passed', async () => { - const response = await request(testHttpServer.app).get( - `${baseUrl}/authRequired` - ); - expect(response.status).toEqual(401); + it('should 200 when valid cookie is provided', async () => { + const response = await request(testHttpServer.app) + .get(`${baseUrl}/authRequired`) + .set( + 'Cookie', + `${idTokenCookieName}=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QifQ.eyJpc3MiOiJtb3ZpZS1yZWl2ZXctY29tbWVudCIsImlhdCI6MTcwNDAzNjQ5MywibmJmIjoxNzA0MDM2NDkzLCJleHAiOjE3MDY2Mjg0OTMsInVzZXJJZCI6IjU3Y2YyN2Q4LTBjNzQtNDk2My05NGE3LTQ3YzM1NzQxZWQwNiIsIm5pY2tuYW1lIjoi64-E7KCE7KCB7J24IO2PieuhoOqwgCDsl7DrkZDruZsg7ZWY66eIIiwidGFnIjoiI0FaN0oiLCJpZHAiOiJHT09HTEUiLCJlbWFpbCI6ImRvd2hhdHlvdXdhbnQwNzI0QGdtYWlsLmNvbSIsImFjY2Vzc0xldmVsIjoiVVNFUiJ9.NX10zbQRCNr-38tGeqV_nBx5VDqTC0uj3k15QuWQYbgBvZh1yIJp3PqmnEuXuuc8Jbm5HZC3p_exVopgAXLLfMqkleNOBh1KB46yQAeJEzl1H-LWPHGRHks_pGaSg0E2irqUCOsCnVAuzkYTefCnsQy7dXamsRr7g9pRppFyRyjzDWY1r5HsEiIw77GUNOZ31f87PkLTJaQHfBvXm5fyS541IIKXMkH_7DaoSjUBTPZOBpPGg3q9hjTbKn2mkESC8Y4E9KFlX9UwoTRtKFI8QaBPYPYvL4ZuKD1pJCEj7nbT0NPnRcC0M6eI0tIDHbTSbMDbSE-HZZrcwn9WYPVqNg` + ) + .expect(200); + + expect(response.body.message).toEqual('Authentication verified'); + }); + + it('should 401 when the Authorization header is not in the Bearer format', async () => { + const response = await request(testHttpServer.app) + .get(`${baseUrl}/authRequired`) + .set('Authorization', 'Test') + .expect(401); + + expect(response.body.messages).toBeDefined(); + expect(response.body.messages).toEqual([ + 'Authorization header must be in Bearer format' + ]); + }); + + it('should 401 when the cookie does not exist', async () => { + const response = await request(testHttpServer.app) + .get(`${baseUrl}/authRequired`) + .expect(401); + + expect(response.body.messages).toBeDefined(); + expect(response.body.messages).toEqual(['cookie does not exist']); }); it('should 404 when no matching route found', async () => {