Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: extractCookie from GraphiQLHeader (#6894) #6969

Merged
merged 8 commits into from
Nov 28, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -1505,13 +1505,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/auth-providers-api/src/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/auth-providers-api/src/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