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

feat(dbAuth): Refactor dbAuthHandler to support WebAPI Request events #9835

Merged
merged 45 commits into from
Jan 25, 2024
Merged
Changes from 29 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9e86135
Simple auth state parsing with middleware in entry server
dac09 Oct 25, 2023
5f1deed
NOT WORKING: try extracting session passed into a cookie
dac09 Oct 26, 2023
bb0b36e
Merge branch 'main' of github.com:redwoodjs/redwood into try/dbauth-ssr
dac09 Nov 2, 2023
9fafcf8
SHIP IT
dac09 Nov 2, 2023
2b66173
Get it building again
dac09 Nov 15, 2023
4bb543a
Merge branch 'main' of github.com:redwoodjs/redwood into try/dbauth-s…
dac09 Nov 15, 2023
a8f0e33
Fix some merged changes
dac09 Nov 15, 2023
37adfa5
Make it forward cookies from authState
dac09 Nov 20, 2023
0542ca7
Merge branch 'main' of github.com:redwoodjs/redwood into try/dbauth-s…
dac09 Dec 8, 2023
b275d9f
Fix building!
dac09 Dec 8, 2023
67c4ace
I think I have it working with failing tests
dac09 Dec 11, 2023
82badfd
Fix authContext tests and dbauth handler
dac09 Dec 20, 2023
3f0a32b
Fix more tests
dac09 Dec 22, 2023
0979725
Get it working with middleware!
dac09 Dec 22, 2023
1a3cbd4
Undo decoder type changes
dac09 Dec 22, 2023
fe40f61
Cleanup isFetchRequest
dac09 Dec 22, 2023
5e0c1c2
Change detection of fetch event
dac09 Dec 25, 2023
4e268aa
Refactor and reuse getEventHeader
dac09 Dec 25, 2023
e05a906
Add nx to gitignore
dac09 Dec 28, 2023
9b1affd
Merge branch 'main' of github.com:redwoodjs/redwood into feat/dbauth-…
dac09 Jan 2, 2024
d25ed51
Merge branch 'main' of github.com:redwoodjs/redwood into feat/dbauth-…
dac09 Jan 3, 2024
0d62479
ServerAuthState types and stuff
dac09 Jan 4, 2024
5ce1870
Merge branch 'main' of github.com:redwoodjs/redwood into feat/dbauth-…
dac09 Jan 9, 2024
be84ace
Restore unneeded changes
dac09 Jan 16, 2024
c3a2ebb
Merge branch 'main' of github.com:redwoodjs/redwood into feat/dbauth-…
dac09 Jan 16, 2024
6a31c00
Just keep the dbAuth fetch-api related changes, revert everything else
dac09 Jan 16, 2024
d773116
Merge main
dac09 Jan 18, 2024
8750800
FIX ALL THE TESTS!
dac09 Jan 18, 2024
3cdb37d
Update packages/auth-providers/dbAuth/api/src/decoder.ts
dac09 Jan 18, 2024
2bdb5e0
Cleanup, update shared.test
dac09 Jan 19, 2024
38b3584
Remove old comment
dac09 Jan 19, 2024
06c07dc
Undo bearer token change
dac09 Jan 19, 2024
bf2b589
Remove unused function
dac09 Jan 19, 2024
abf3a95
Merge branch 'main' into feat/dbauth-fetch-handler
dac09 Jan 19, 2024
7491bdf
Merge branch 'main' into feat/dbauth-fetch-handler
dac09 Jan 21, 2024
5900629
Merge branch 'main' of github.com:redwoodjs/redwood into feat/dbauth-…
dac09 Jan 21, 2024
b32eca7
Fix merge after vitest
dac09 Jan 21, 2024
61d4664
Merge branch 'feat/dbauth-fetch-handler' of github.com:dac09/redwood …
dac09 Jan 21, 2024
8fdbec0
Merge branch 'main' into feat/dbauth-fetch-handler
dac09 Jan 22, 2024
ed74729
Merge branch 'main' into feat/dbauth-fetch-handler
dac09 Jan 22, 2024
d4ce855
Merge branch 'main' into feat/dbauth-fetch-handler
dac09 Jan 22, 2024
d35207f
Remove this.init from inside class, call them in the tests instead
dac09 Jan 23, 2024
81b787f
Merge branch 'main' of github.com:redwoodjs/redwood into feat/dbauth-…
dac09 Jan 23, 2024
616bd22
Also convert fetch handler test to vitest
dac09 Jan 23, 2024
65e864a
Merge branch 'main' into feat/dbauth-fetch-handler
dac09 Jan 25, 2024
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
127 changes: 97 additions & 30 deletions packages/api/src/__tests__/normalizeRequest.test.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import type { APIGatewayProxyEvent } from 'aws-lambda'

import { normalizeRequest } from '../transforms'

export const createMockedEvent = (
export const createMockedLambdaEvent = (
httpMethod = 'POST',
body: any = undefined,
isBase64Encoded = false
@@ -53,41 +53,108 @@ export const createMockedEvent = (
}
}

test('Normalizes an aws event with base64', () => {
const corsEventB64 = createMockedEvent(
'POST',
Buffer.from(JSON.stringify({ bazinga: 'hello_world' }), 'utf8').toString(
'base64'
),
true
)

expect(normalizeRequest(corsEventB64)).toEqual({
headers: new Headers(corsEventB64.headers),
method: 'POST',
query: null,
body: {
bazinga: 'hello_world',
},
describe('Lambda Request', () => {
it('Normalizes an aws event with base64', async () => {
const corsEventB64 = createMockedLambdaEvent(
'POST',
Buffer.from(JSON.stringify({ bazinga: 'hello_world' }), 'utf8').toString(
'base64'
),
true
)

expect(await normalizeRequest(corsEventB64)).toEqual({
headers: new Headers(corsEventB64.headers as Record<string, string>),
method: 'POST',
query: null,
jsonBody: {
bazinga: 'hello_world',
},
})
})

it('Handles CORS requests with and without b64 encoded', async () => {
const corsEventB64 = createMockedLambdaEvent('OPTIONS', undefined, true)

expect(await normalizeRequest(corsEventB64)).toEqual({
headers: new Headers(corsEventB64.headers as Record<string, string>), // headers returned as symbol
method: 'OPTIONS',
query: null,
jsonBody: {},
})

const corsEventWithoutB64 = createMockedLambdaEvent(
'OPTIONS',
undefined,
false
)

expect(await normalizeRequest(corsEventWithoutB64)).toEqual({
headers: new Headers(corsEventB64.headers as Record<string, string>), // headers returned as symbol
method: 'OPTIONS',
query: null,
jsonBody: {},
})
})
})

test('Handles CORS requests with and without b64 encoded', () => {
const corsEventB64 = createMockedEvent('OPTIONS', undefined, true)
describe('Fetch API Request', () => {
it('Normalizes a fetch event', async () => {
const fetchEvent = new Request(
'http://localhost:9210/graphql?whatsup=doc&its=bugs',
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ bazinga: 'kittens_purr_purr' }),
}
)

expect(normalizeRequest(corsEventB64)).toEqual({
headers: new Headers(corsEventB64.headers), // headers returned as symbol
method: 'OPTIONS',
query: null,
body: undefined,
const partial = await normalizeRequest(fetchEvent)

expect(partial).toMatchObject({
// headers: fetchEvent.headers,
method: 'POST',
query: {
whatsup: 'doc',
its: 'bugs',
},
jsonBody: {
bazinga: 'kittens_purr_purr',
},
})

expect(partial.headers.get('content-type')).toEqual('application/json')
})

const corsEventWithoutB64 = createMockedEvent('OPTIONS', undefined, false)
it('Handles an empty body', async () => {
const headers = {
'content-type': 'application/json',
'x-custom-header': 'bazinga',
}

const fetchEvent = new Request(
'http://localhost:9210/graphql?whatsup=doc&its=bugs',
{
method: 'PUT',
headers,
body: '',
}
)

const partial = await normalizeRequest(fetchEvent)

expect(partial).toMatchObject({
method: 'PUT',
query: {
whatsup: 'doc',
its: 'bugs',
},
jsonBody: {}, // @NOTE empty body is {} not undefined
})

expect(normalizeRequest(corsEventWithoutB64)).toEqual({
headers: new Headers(corsEventB64.headers), // headers returned as symbol
method: 'OPTIONS',
query: null,
body: undefined,
expect(partial.headers.get('content-type')).toEqual(headers['content-type'])
expect(partial.headers.get('x-custom-header')).toEqual('bazinga')
})
})
35 changes: 25 additions & 10 deletions packages/api/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -2,18 +2,22 @@ export * from './parseJWT'

import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'

import { getEventHeader } from '../event'

import type { Decoded } from './parseJWT'
export type { Decoded }

// This is shared by `@redwoodjs/web`
const AUTH_PROVIDER_HEADER = 'auth-provider'

export const getAuthProviderHeader = (event: APIGatewayProxyEvent) => {
export const getAuthProviderHeader = (
event: APIGatewayProxyEvent | Request
) => {
const authProviderKey = Object.keys(event?.headers ?? {}).find(
(key) => key.toLowerCase() === AUTH_PROVIDER_HEADER
)
if (authProviderKey) {
return event?.headers[authProviderKey]
return getEventHeader(event, authProviderKey)
}
return undefined
}
@@ -27,11 +31,9 @@ export interface AuthorizationHeader {
* Split the `Authorization` header into a schema and token part.
*/
export const parseAuthorizationHeader = (
event: APIGatewayProxyEvent
event: APIGatewayProxyEvent | Request
): AuthorizationHeader => {
const parts = (
event.headers?.authorization || event.headers?.Authorization
)?.split(' ')
const parts = getEventHeader(event, 'authorization')?.split(' ')
if (parts?.length !== 2) {
throw new Error('The `Authorization` header is not valid.')
}
@@ -42,16 +44,24 @@ export const parseAuthorizationHeader = (
return { schema, token }
}

/** @MARK Note that we do not send LambdaContext when making fetch requests
*
* This part is incomplete, as we need to decide how we will make the breaking change to
* 1. getCurrentUser
* 2. authDecoders

*/

export type AuthContextPayload = [
Decoded,
{ type: string } & AuthorizationHeader,
{ event: APIGatewayProxyEvent; context: LambdaContext }
{ event: APIGatewayProxyEvent | Request; context: LambdaContext }
]

export type Decoder = (
token: string,
type: string,
req: { event: APIGatewayProxyEvent; context: LambdaContext }
req: { event: APIGatewayProxyEvent | Request; context: LambdaContext }
) => Promise<Decoded>

/**
@@ -64,7 +74,7 @@ export const getAuthenticationContext = async ({
context,
}: {
authDecoder?: Decoder | Decoder[]
event: APIGatewayProxyEvent
event: APIGatewayProxyEvent | Request
context: LambdaContext
}): Promise<undefined | AuthContextPayload> => {
const type = getAuthProviderHeader(event)
@@ -89,7 +99,12 @@ export const getAuthenticationContext = async ({

let i = 0
while (!decoded && i < authDecoders.length) {
decoded = await authDecoders[i](token, type, { event, context })
decoded = await authDecoders[i](token, type, {
// @TODO: We will need to make a breaking change to support `Request` objects.
// We can remove this typecast
event: event,
context,
})
i++
}

6 changes: 3 additions & 3 deletions packages/api/src/cors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Headers } from '@whatwg-node/fetch'

import type { Request } from './transforms'
import type { PartialRequest } from './transforms'

export type CorsConfig = {
origin?: boolean | string | string[]
@@ -59,10 +59,10 @@ export function createCorsContext(cors: CorsConfig | undefined) {
}

return {
shouldHandleCors(request: Request) {
shouldHandleCors(request: PartialRequest) {
return request.method === 'OPTIONS'
},
getRequestHeaders(request: Request): CorsHeaders {
getRequestHeaders(request: PartialRequest): CorsHeaders {
const eventHeaders = new Headers(request.headers as HeadersInit)
const requestCorsHeaders = new Headers(corsHeaders)

15 changes: 15 additions & 0 deletions packages/api/src/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { APIGatewayProxyEvent } from 'aws-lambda'

import { isFetchApiRequest } from './transforms'

// Extracts the header from an event, handling lower and upper case header names.
export const getEventHeader = (
event: APIGatewayProxyEvent | Request,
headerName: string
) => {
if (isFetchApiRequest(event)) {
return event.headers.get(headerName)
}

return event.headers[headerName] || event.headers[headerName.toLowerCase()]
}
1 change: 1 addition & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ export * from './types'

export * from './transforms'
export * from './cors'
export * from './event'

// @NOTE: use require, to avoid messing around with tsconfig and nested output dirs
const packageJson = require('../package.json')
85 changes: 75 additions & 10 deletions packages/api/src/transforms.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Headers } from '@whatwg-node/fetch'
import { Headers, Request as PonyfillRequest } from '@whatwg-node/fetch'
import type { APIGatewayProxyEvent } from 'aws-lambda'

// This is the same interface used by GraphQL Yoga
// But not importing here to avoid adding a dependency
export interface Request {
body?: any
// This is part of the request, dreived either from a LambdaEvent or FetchAPI Request
// We do this to keep the API consistent between the two
// When we support only the FetchAPI request, we should remove this
export interface PartialRequest<TBody = Record<string, any>> {
jsonBody: TBody
headers: Headers
method: string
query: any
@@ -13,9 +14,9 @@ export interface Request {
/**
* Extracts and parses body payload from event with base64 encoding check
*/
export const parseEventBody = (event: APIGatewayProxyEvent) => {
export const parseLambdaEventBody = (event: APIGatewayProxyEvent) => {
if (!event.body) {
return
return {}
}

if (event.isBase64Encoded) {
@@ -25,14 +26,78 @@ export const parseEventBody = (event: APIGatewayProxyEvent) => {
}
}

export function normalizeRequest(event: APIGatewayProxyEvent): Request {
const body = parseEventBody(event)
/**
* Extracts and parses body payload from Fetch Request
* with check for empty body
*
* NOTE: whatwg/server expects that you will decode the base64 body yourself
* see readme here: https://github.com/ardatan/whatwg-node/tree/master/packages/server#aws-lambda
*/
export const parseFetchEventBody = async (event: Request) => {
if (!event.body) {
return {}
}

const body = await event.text()

return body ? JSON.parse(body) : {}
}

export const isFetchApiRequest = (
event: Request | APIGatewayProxyEvent
): event is Request => {
if (
event.constructor.name === 'Request' ||
event.constructor.name === PonyfillRequest.name
) {
return true
}

// Also do an extra check on type of headers
if (Symbol.iterator in Object(event.headers)) {
return true
}

return false
}

function getQueryStringParams(reqUrl: string) {
const url = new URL(reqUrl)
const params = new URLSearchParams(url.search)

const paramObject: Record<string, string> = {}
for (const entry of params.entries()) {
paramObject[entry[0]] = entry[1] // each 'entry' is a [key, value] tuple
}
return paramObject
}

/**
*
* This function returns a an object that lets you access _some_ of the request properties in a consistent way
* You can give it either a LambdaEvent or a Fetch API Request
*
* NOTE: It does NOT return a full Request object!
*/
export async function normalizeRequest(
event: APIGatewayProxyEvent | Request
): Promise<PartialRequest> {
if (isFetchApiRequest(event)) {
return {
headers: event.headers,
method: event.method,
query: getQueryStringParams(event.url),
jsonBody: await parseFetchEventBody(event),
}
}

const jsonBody = parseLambdaEventBody(event)

return {
headers: new Headers(event.headers as Record<string, string>),
method: event.httpMethod,
query: event.queryStringParameters,
body,
jsonBody,
}
}

Loading