Skip to content

Commit

Permalink
fix: extractCookie from GraphiQLHeader (#6894) (#6969)
Browse files Browse the repository at this point in the history
Co-authored-by: Aaron So <zaiyou12@gmail.com>
  • Loading branch information
2 people authored and jtoar committed Dec 8, 2022
1 parent 375bcb4 commit c32085b
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 12 deletions.
87 changes: 87 additions & 0 deletions packages/api/src/functions/dbAuth/__tests__/DbAuthHandler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1500,13 +1500,100 @@ describe('dbAuth', () => {
),
},
}

const dbAuth = new DbAuthHandler(event, context, options)
const response = await dbAuth.getToken()

expect(response[0]).toEqual('{"error":"User not found"}')
})
})

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 = {
Expand Down
102 changes: 102 additions & 0 deletions packages/api/src/functions/dbAuth/__tests__/shared.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import CryptoJS from 'crypto-js'

import * as error from '../errors'
import {
extractCookie,
getSession,
hashPassword,
decryptSession,
Expand Down Expand Up @@ -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)
})
})
})
})
36 changes: 24 additions & 12 deletions packages/api/src/functions/dbAuth/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit c32085b

Please sign in to comment.