diff --git a/packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js b/packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js index deac5d412ad9..56623467d51f 100644 --- a/packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js +++ b/packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js @@ -1500,6 +1500,7 @@ describe('dbAuth', () => { ), }, } + const dbAuth = new DbAuthHandler(event, context, options) const response = await dbAuth.getToken() @@ -1507,6 +1508,92 @@ describe('dbAuth', () => { }) }) + describe('When a developer has set GraphiQL headers to mock a session cookie', () => { + describe('when in development environment', () => { + const curNodeEnv = process.env.NODE_ENV + + beforeAll(() => { + // Session cookie from graphiQLHeaders only extracted in dev + process.env.NODE_ENV = 'development' + }) + + afterAll(() => { + process.env.NODE_ENV = curNodeEnv + expect(process.env.NODE_ENV).toBe('test') + }) + + it('authenticates the user based on GraphiQL headers when no event.headers present', async () => { + // setup graphiQL header cookie in extensions + const dbUser = await createDbUser() + event.body = JSON.stringify({ + extensions: { + headers: { + 'auth-provider': 'dbAuth', + cookie: encryptToCookie(JSON.stringify({ id: dbUser.id })), + authorization: 'Bearer ' + dbUser.id, + }, + }, + }) + + const dbAuth = new DbAuthHandler(event, context, options) + const user = await dbAuth._getCurrentUser() + expect(user.id).toEqual(dbUser.id) + }) + + it('Cookie from GraphiQLHeaders takes precedence over event headers when authenticating user', async () => { + // setup session cookie in GraphiQL header + const dbUser = await createDbUser() + const dbUserId = dbUser.id + + event.body = JSON.stringify({ + extensions: { + headers: { + 'auth-provider': 'dbAuth', + cookie: encryptToCookie(JSON.stringify({ id: dbUserId })), + authorization: 'Bearer ' + dbUserId, + }, + }, + }) + + // create session cookie in event header + event.headers.cookie = encryptToCookie( + JSON.stringify({ id: 9999999999 }) + ) + + // should read session from graphiQL header, not from cookie + const dbAuth = new DbAuthHandler(event, context, options) + const user = await dbAuth._getCurrentUser() + expect(user.id).toEqual(dbUserId) + }) + }) + + describe('when in test/production environment and graphiqlHeader sets a session cookie', () => { + it("isn't used to authenticate a user", async () => { + const dbUser = await createDbUser() + const dbUserId = dbUser.id + + event.body = JSON.stringify({ + extensions: { + headers: { + 'auth-provider': 'dbAuth', + cookie: encryptToCookie(JSON.stringify({ id: dbUserId })), + authorization: 'Bearer ' + dbUserId, + }, + }, + }) + + try { + const dbAuth = new DbAuthHandler(event, context, options) + await dbAuth._getCurrentUser() + } catch (e) { + expect(e.message).toEqual( + 'Cannot retrieve user details without being logged in' + ) + } + }) + }) + }) + describe('webAuthnAuthenticate', () => { it('throws an error if WebAuthn options are not defined', async () => { event = { diff --git a/packages/api/src/functions/dbAuth/__tests__/shared.test.js b/packages/api/src/functions/dbAuth/__tests__/shared.test.js index 25e71355de3d..2dfeae6a407e 100644 --- a/packages/api/src/functions/dbAuth/__tests__/shared.test.js +++ b/packages/api/src/functions/dbAuth/__tests__/shared.test.js @@ -2,6 +2,7 @@ import CryptoJS from 'crypto-js' import * as error from '../errors' import { + extractCookie, getSession, hashPassword, decryptSession, @@ -120,4 +121,105 @@ describe('hashPassword', () => { expect(salt).toMatch(/^[a-f0-9]+$/) expect(salt.length).toEqual(32) }) + + describe('session cookie extraction', () => { + let event + + const encryptToCookie = (data) => { + return `session=${CryptoJS.AES.encrypt(data, process.env.SESSION_SECRET)}` + } + + beforeEach(() => { + event = { + queryStringParameters: {}, + path: '/.redwood/functions/auth', + headers: {}, + } + }) + + it('extracts from the event', () => { + const cookie = encryptToCookie( + JSON.stringify({ id: 9999999999 }) + ';' + 'token' + ) + + event = { + headers: { + cookie, + }, + } + + expect(extractCookie(event)).toEqual(cookie) + }) + + it('extract cookie handles non-JSON event body', () => { + event.body = '' + + expect(extractCookie(event)).toBeUndefined() + }) + + describe('when in development', () => { + const curNodeEnv = process.env.NODE_ENV + + beforeAll(() => { + // Session cookie from graphiQLHeaders only extracted in dev + process.env.NODE_ENV = 'development' + }) + + afterAll(() => { + process.env.NODE_ENV = curNodeEnv + event = {} + expect(process.env.NODE_ENV).toBe('test') + }) + + it('extract cookie handles non-JSON event body', () => { + event.body = '' + + expect(extractCookie(event)).toBeUndefined() + }) + + it('extracts GraphiQL cookie from the header extensions', () => { + const dbUserId = 42 + + const cookie = encryptToCookie(JSON.stringify({ id: dbUserId })) + event.body = JSON.stringify({ + extensions: { + headers: { + 'auth-provider': 'dbAuth', + cookie, + authorization: 'Bearer ' + dbUserId, + }, + }, + }) + + expect(extractCookie(event)).toEqual(cookie) + }) + + it('overwrites cookie with event header GraphiQL when in dev', () => { + const sessionCookie = encryptToCookie( + JSON.stringify({ id: 9999999999 }) + ';' + 'token' + ) + + event = { + headers: { + cookie: sessionCookie, + }, + } + + const dbUserId = 42 + + const cookie = encryptToCookie(JSON.stringify({ id: dbUserId })) + event.body = JSON.stringify({ + extensions: { + headers: { + 'auth-provider': 'dbAuth', + cookie, + authorization: 'Bearer ' + dbUserId, + }, + }, + }) + + expect(extractCookie(event)).toEqual(cookie) + }) + }) + }) }) diff --git a/packages/api/src/functions/dbAuth/shared.ts b/packages/api/src/functions/dbAuth/shared.ts index b69d83abcd97..1a005f4c4e59 100644 --- a/packages/api/src/functions/dbAuth/shared.ts +++ b/packages/api/src/functions/dbAuth/shared.ts @@ -3,22 +3,34 @@ import CryptoJS from 'crypto-js' import * as DbAuthError from './errors' -// Extracts the cookie from an event, handling lower and upper case header -// names. -// Checks for cookie in headers in dev when user has generated graphiql headers -export const extractCookie = (event: APIGatewayProxyEvent) => { - let cookieFromGraphiqlHeader +// Extracts the cookie from an event, handling lower and upper case header names. +const eventHeadersCookie = (event: APIGatewayProxyEvent) => { + return event.headers.cookie || event.headers.Cookie +} + +// When in development environment, check for cookie in the request extension headers +// if user has generated graphiql headers +const eventGraphiQLHeadersCookie = (event: APIGatewayProxyEvent) => { if (process.env.NODE_ENV === 'development') { try { - cookieFromGraphiqlHeader = JSON.parse(event.body ?? '{}').extensions - ?.headers?.cookie - } catch (e) { - return event.headers.cookie || event.headers.Cookie + const jsonBody = JSON.parse(event.body ?? '{}') + return ( + jsonBody?.extensions?.headers?.cookie || + jsonBody?.extensions?.headers?.Cookie + ) + } catch { + // sometimes the event body isn't json + return } } - return ( - event.headers.cookie || event.headers.Cookie || cookieFromGraphiqlHeader - ) + + return +} + +// Extracts the session cookie from an event, handling both +// development environment GraphiQL headers and production environment headers. +export const extractCookie = (event: APIGatewayProxyEvent) => { + return eventGraphiQLHeadersCookie(event) || eventHeadersCookie(event) } // decrypts the session cookie and returns an array: [data, csrf]