Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/jwtUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions src/server/context.spec.ts
Original file line number Diff line number Diff line change
@@ -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'),
);
});
});
});
36 changes: 26 additions & 10 deletions src/server/context.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
token?: string;
Expand All @@ -18,20 +21,33 @@ export async function getAppContext(
{ req },
publicKeys: Record<string, string>,
): Promise<IContext> {
//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');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we throw a 401 instead of a generic error, to have accurate error statistics?

Copy link
Copy Markdown
Collaborator Author

@jpetto jpetto May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i waffled between telling a potential attacker they were unauthorized versus giving them no additional info and (obviously) landed on the latter. i'm good with it as-is for now. we'll get sentry counts for this scenario.

}

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
Expand Down
20 changes: 17 additions & 3 deletions src/server/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions src/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ async function startServer() {
// enable cross-site request forgery protection
csrfPrevention: true,
});

await server.start();

return server;
}

Expand Down
Loading