Skip to content

Commit

Permalink
fix(dbauth): multiValueHeaders (#10889)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Jun 28, 2024
1 parent 91519a6 commit 5ba9ed2
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 35 deletions.
34 changes: 25 additions & 9 deletions packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ import {

import * as DbAuthError from './errors'
import {
buildDbAuthResponse,
cookieName,
decryptSession,
encryptSession,
extractCookie,
extractHashingOptions,
getDbAuthResponseBuilder,
getSession,
hashPassword,
hashToken,
Expand Down Expand Up @@ -303,6 +303,7 @@ type Params = AuthenticationResponseJSON &
}

type DbAuthSession<T = unknown> = Record<string, T>
type CorsHeaders = Record<string, string>

const DEFAULT_ALLOWED_USER_FIELDS = ['id', 'email']

Expand All @@ -326,12 +327,25 @@ export class DbAuthHandler<
sessionExpiresDate: string
webAuthnExpiresDate: string
encryptedSession: string | null = null
createResponse: (
response: {
body?: string
statusCode: number
headers?: Headers
},
corsHeaders: CorsHeaders,
) => {
headers: Record<string, string | string[]>
body?: string | undefined
statusCode: number
}

public get normalizedRequest() {
if (!this._normalizedRequest) {
// This is a dev time error, no need to throw a specialized error
throw new Error(
'dbAuthHandler has not been initialised. Either await dbAuthHandler.invoke() or call await dbAuth.init()',
'dbAuthHandler has not been initialized. Either await ' +
'dbAuthHandler.invoke() or call await dbAuth.init()',
)
}
return this._normalizedRequest
Expand Down Expand Up @@ -426,6 +440,8 @@ export class DbAuthHandler<

this.cookie = extractCookie(event) || ''

this.createResponse = getDbAuthResponseBuilder(event)

this._validateOptions()

this.db = this.options.db
Expand Down Expand Up @@ -494,14 +510,14 @@ export class DbAuthHandler<
corsHeaders = this.corsContext.getRequestHeaders(this.normalizedRequest)
// Return CORS headers for OPTIONS requests
if (this.corsContext.shouldHandleCors(this.normalizedRequest)) {
return buildDbAuthResponse({ body: '', statusCode: 200 }, corsHeaders)
return this.createResponse({ body: '', statusCode: 200 }, corsHeaders)
}
}

// if there was a problem decryption the session, just return the logout
// response immediately
if (this.hasInvalidSession) {
return buildDbAuthResponse(
return this.createResponse(
this._ok(...this._logoutResponse()),
corsHeaders,
)
Expand All @@ -512,24 +528,24 @@ export class DbAuthHandler<

// get the auth method the incoming request is trying to call
if (!DbAuthHandler.METHODS.includes(method)) {
return buildDbAuthResponse(this._notFound(), corsHeaders)
return this.createResponse(this._notFound(), corsHeaders)
}

// make sure it's using the correct verb, GET vs POST
if (this.httpMethod !== DbAuthHandler.VERBS[method]) {
return buildDbAuthResponse(this._notFound(), corsHeaders)
return this.createResponse(this._notFound(), corsHeaders)
}

// call whatever auth method was requested and return the body and headers
const [body, headers, options = { statusCode: 200 }] =
await this[method]()

return buildDbAuthResponse(this._ok(body, headers, options), corsHeaders)
return this.createResponse(this._ok(body, headers, options), corsHeaders)
} catch (e: any) {
if (e instanceof DbAuthError.WrongVerbError) {
return buildDbAuthResponse(this._notFound(), corsHeaders)
return this.createResponse(this._notFound(), corsHeaders)
} else {
return buildDbAuthResponse(
return this.createResponse(
this._badRequest(e.message || e),
corsHeaders,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { APIGatewayProxyEvent } from 'aws-lambda'
import { describe, it, expect } from 'vitest'

import { buildDbAuthResponse } from '../shared'
import { getDbAuthResponseBuilder } from '../shared'

describe('buildDbAuthResponse', () => {
it('should add cors headers and set-cookie as array to the response', () => {
it('should add cors headers and set-cookie as array to the response to Requests', () => {
const resHeaders = new Headers({
header1: 'value1',
header2: 'value2',
Expand Down Expand Up @@ -33,7 +34,48 @@ describe('buildDbAuthResponse', () => {
},
}

const result = buildDbAuthResponse(response, corsHeaders)
const createResponse = getDbAuthResponseBuilder({} as Request)
const result = createResponse(response, corsHeaders)

expect(result).toEqual(expectedResponse)
})

it('should add cors headers and set-cookie as multiValueHeaders array to the response to APIGatewayProxyEvent', () => {
const resHeaders = new Headers({
header1: 'value1',
header2: 'value2',
})

resHeaders.append('set-cookie', 'cookie1=value1')
resHeaders.append('set-cookie', 'cookie2=value2')

const response = {
statusCode: 200,
headers: resHeaders,
}

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
}

const expectedResponse = {
statusCode: 200,
headers: {
header1: 'value1',
header2: 'value2',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
},
multiValueHeaders: {
'Set-Cookie': ['cookie1=value1', 'cookie2=value2'],
},
}

const createResponse = getDbAuthResponseBuilder({
multiValueHeaders: {},
} as unknown as APIGatewayProxyEvent)
const result = createResponse(response, corsHeaders)

expect(result).toEqual(expectedResponse)
})
Expand Down Expand Up @@ -62,7 +104,8 @@ describe('buildDbAuthResponse', () => {
},
}

const result = buildDbAuthResponse(response, corsHeaders)
const createResponse = getDbAuthResponseBuilder({} as Request)
const result = createResponse(response, corsHeaders)

expect(result).toEqual(expectedResponse)
})
Expand Down
64 changes: 42 additions & 22 deletions packages/auth-providers/dbAuth/api/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,33 +259,53 @@ export const cookieName = (name: string | undefined) => {
}

/**
* Returns a lambda response!
* Returns a builder for a lambda response
*
* This is used as the final call to return a response from the handler.
* This is used as the final call to return a response from the dbAuth handler
*
* Converts "Set-Cookie" headers to an array of strings.
* Converts "Set-Cookie" headers to an array of strings or a multiValueHeaders
* object
*/
export const buildDbAuthResponse = (
response: {
body?: string
statusCode: number
headers?: Headers
},
corsHeaders: CorsHeaders,
) => {
const setCookieHeaders = response.headers?.getSetCookie() || []

return {
...response,
headers: {
export function getDbAuthResponseBuilder(
event: APIGatewayProxyEvent | Request,
) {
return (
response: {
body?: string
statusCode: number
headers?: Headers
},
corsHeaders: CorsHeaders,
) => {
const headers: Record<string, string | Array<string>> = {
...Object.fromEntries(response.headers?.entries() || []),
...(setCookieHeaders.length > 0
? {
'set-cookie': setCookieHeaders,
}
: {}),
...corsHeaders,
},
}

const dbAuthResponse: {
statusCode: number
headers: Record<string, string | Array<string>>
multiValueHeaders?: Record<string, Array<string>>
body?: string
} = {
...response,
headers,
}

const setCookieHeaders = response.headers?.getSetCookie() || []

if (setCookieHeaders.length > 0) {
if ('multiValueHeaders' in event) {
dbAuthResponse.multiValueHeaders = {
'Set-Cookie': setCookieHeaders,
}
delete headers['set-cookie']
} else {
headers['set-cookie'] = setCookieHeaders
}
}

return dbAuthResponse
}
}

Expand Down

0 comments on commit 5ba9ed2

Please sign in to comment.