From 878b377ed5648c6d5ff1414eb56ce2df338201be Mon Sep 17 00:00:00 2001 From: Jonathan Petto Date: Mon, 4 May 2026 13:50:41 -0500 Subject: [PATCH] feat: reject all requests without a JWT - fix sentry import in jwtUtils (this was never working?) - add tests for getAppContext --- src/jwtUtils.ts | 2 +- src/server/context.spec.ts | 56 ++++++++++++++++++++++++++++++++++++++ src/server/context.ts | 36 +++++++++++++++++------- src/server/main.spec.ts | 20 ++++++++++++-- src/server/main.ts | 2 ++ 5 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 src/server/context.spec.ts diff --git a/src/jwtUtils.ts b/src/jwtUtils.ts index 5f846a02..e4d74ee7 100644 --- a/src/jwtUtils.ts +++ b/src/jwtUtils.ts @@ -7,7 +7,7 @@ import jwt, { import jwksClient from 'jwks-rsa'; import { AuthenticationError } from 'apollo-server-errors'; import config from './config'; -import Sentry from '@sentry/node'; +import * as Sentry from '@sentry/node'; /** * Properties of the identity property in CognitoUser below diff --git a/src/server/context.spec.ts b/src/server/context.spec.ts new file mode 100644 index 00000000..b64969c1 --- /dev/null +++ b/src/server/context.spec.ts @@ -0,0 +1,56 @@ +import sinon from 'sinon'; + +import * as jwtUtils from '../jwtUtils'; +import { getAppContext } from './context'; + +describe('context', () => { + describe('getAppContext', () => { + beforeAll(() => { + // mock JWT validation + sinon.stub(jwtUtils, 'validateAndGetAdminAPIUser').resolves({ + name: 'test name', + groups: ['test group'], + username: 'testUserName', + }); + }); + + afterAll(() => { + sinon.restore(); + }); + + it('should return a context if the request has a JWT', async () => { + const request = { + headers: { + authorization: 'Bearer TestJWT', + }, + }; + + const publicKeys = { + test: 'test', + }; + + const context = await getAppContext({ req: request }, publicKeys); + + // context should have expected properties set + expect(context.token).not.toBeNull(); + expect(context.forwardHeaders).not.toBeNull(); + }); + + it('should throw if the request does not have a JWT', async () => { + const request = { + headers: { + someNonAuthorizationHeader: 'someValue', + }, + }; + + const publicKeys = { + test: 'test', + }; + + // creating context should throw if authorization header is missing + await expect(getAppContext({ req: request }, publicKeys)).rejects.toThrow( + new Error('Internal server error'), + ); + }); + }); +}); diff --git a/src/server/context.ts b/src/server/context.ts index 1a6c3da3..6a072c07 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -1,9 +1,12 @@ +import * as Sentry from '@sentry/node'; + import { AdminAPIUser, getSigningKeysFromServer, validateAndGetAdminAPIUser, } from '../jwtUtils'; import { extractHeader } from './requestHelpers'; + export type IContext = { publicKeys?: Record; token?: string; @@ -18,20 +21,33 @@ export async function getAppContext( { req }, publicKeys: Record, ): Promise { - //See if we have an authorization header + // See if we have an authorization header const token = req.headers.authorization ?? null; - const context: IContext = { token, publicKeys }; + // if the request doesn't have a JWT, reject it outright + if (!token) { + // log to ECS + console.log('Request is missing JWT'); + console.log(req); - //OH boy! we have an authorization header, lets pull out our JWT and validate it. - if (token) { - context.token = token.split(' ')[1]; - //AHH we have a user. Lets put it in our request to use elsewhere. - context.adminAPIUser = await validateAndGetAdminAPIUser( - context.token, - publicKeys, - ); + Sentry.captureException('Request is missing JWT'); + + // throw a generic error if request is missing JWT + throw new Error('Internal server error'); } + + const context: IContext = { token, publicKeys }; + + // OH boy! we have an authorization header, lets pull out our JWT and validate it. + context.token = token.split(' ')[1]; + + // AHH we have a user. Lets put it in our request to use elsewhere. + // this will throw if the provided JWT is invalid. + context.adminAPIUser = await validateAndGetAdminAPIUser( + context.token, + publicKeys, + ); + // Add the request headers we want to forward to the subgraphs context.forwardHeaders = { // We want the originating client, which is the leftmost IP address diff --git a/src/server/main.spec.ts b/src/server/main.spec.ts index 3989a16c..6dfdcf6c 100644 --- a/src/server/main.spec.ts +++ b/src/server/main.spec.ts @@ -7,9 +7,23 @@ describe('Context factory function', () => { const keyStub = sinon.stub(jwtUtils, 'getSigningKeysFromServer').resolves({ testKID: 'hereisalongkidstring', }); - await contextFactory({ req: { headers: {} } }); - await contextFactory({ req: { headers: {} } }); - await contextFactory({ req: { headers: {} } }); + + // mock JWT validation + sinon.stub(jwtUtils, 'validateAndGetAdminAPIUser').resolves({ + name: 'test name', + groups: ['test group'], + username: 'testUserName', + }); + + await contextFactory({ + req: { headers: { authorization: 'Bearer TestRawJWT' } }, + }); + await contextFactory({ + req: { headers: { authorization: 'Bearer TestRawJWT' } }, + }); + await contextFactory({ + req: { headers: { authorization: 'Bearer TestRawJWT' } }, + }); expect(keyStub.callCount).toEqual(1); diff --git a/src/server/main.ts b/src/server/main.ts index 432e6307..3a22426f 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -58,7 +58,9 @@ async function startServer() { // enable cross-site request forgery protection csrfPrevention: true, }); + await server.start(); + return server; }