From 8ae4445373ccb977be99e3270ffc8a0b753590a5 Mon Sep 17 00:00:00 2001 From: Krisztiaan Date: Tue, 20 Oct 2020 01:16:26 +0200 Subject: [PATCH 1/3] chore(auth, ts): parseJWT remove wrong types --- packages/api/src/parseJWT.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/api/src/parseJWT.ts b/packages/api/src/parseJWT.ts index 0a09e07157c9..4e69eca5f325 100644 --- a/packages/api/src/parseJWT.ts +++ b/packages/api/src/parseJWT.ts @@ -1,32 +1,36 @@ -const appMetadata = (token: { - decoded: { [index: string]: Record } +type MetaDataBase = { roles?: string[]; authorization?: { roles?: string[] } } + +function appMetadata(token: { + decoded: { [index: string]: any } namespace?: string -}): any => { +}): MetaDataBase { const claim = token.namespace ? `${token.namespace}/app_metadata` : 'app_metadata' return token.decoded?.[claim] || {} } -const roles = (token: { - decoded: { [index: string]: Record } - namespace?: string -}): any => { - const metadata = appMetadata(token) +function roles( + token: { + decoded: { [index: string]: any } + }, + metadata: MetaDataBase +): string[] { return ( - token.decoded?.roles || + ((token.decoded?.roles as unknown) as string[]) || metadata?.roles || metadata.authorization?.roles || [] ) } -export const parseJWT = (token: { - decoded: { [index: string]: Record } +export function parseJWT(token: { + decoded: { [index: string]: any } namespace?: string -}): any => { +}) { + const appMetaData = appMetadata(token) return { - appMetadata: appMetadata(token), - roles: roles(token), + appMetadata: appMetaData, + roles: roles(token, appMetaData), } } From 98b3225cf29e86f4042889f39038f208ec344f98 Mon Sep 17 00:00:00 2001 From: Krisztiaan Date: Tue, 20 Oct 2020 01:18:12 +0200 Subject: [PATCH 2/3] chore(auth): remove redundancy, add null fallbacks --- packages/api/src/auth/decoders/auth0.ts | 8 +------- packages/api/src/auth/decoders/index.ts | 9 +++++---- packages/api/src/auth/index.ts | 11 +++++------ .../fixtures/apiGatewayProxyEvent.fixture.ts | 6 ++---- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/api/src/auth/decoders/auth0.ts b/packages/api/src/auth/decoders/auth0.ts index 4de3dcffe1cf..c4c0e9458e44 100644 --- a/packages/api/src/auth/decoders/auth0.ts +++ b/packages/api/src/auth/decoders/auth0.ts @@ -22,7 +22,7 @@ import jwksClient from 'jwks-rsa' * ^1: https://manage.auth0.com/#/rules/new * */ -export const verifyAuth0Token = ( +export const auth0 = ( bearerToken: string ): Promise> => { return new Promise((resolve, reject) => { @@ -62,9 +62,3 @@ export const verifyAuth0Token = ( ) }) } - -export const auth0 = async ( - token: string -): Promise> => { - return verifyAuth0Token(token) -} diff --git a/packages/api/src/auth/decoders/index.ts b/packages/api/src/auth/decoders/index.ts index 4198c1b6b0f3..1ed34d372a78 100644 --- a/packages/api/src/auth/decoders/index.ts +++ b/packages/api/src/auth/decoders/index.ts @@ -1,12 +1,13 @@ -import type { GlobalContext } from 'src/globalContext' import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda' import type { SupportedAuthTypes } from '@redwoodjs/auth' +import type { GlobalContext } from '../../globalContext' -import { netlify } from './netlify' import { auth0 } from './auth0' +import { netlify } from './netlify' + const noop = (token: string) => token -const typesToDecoders: Record = { +const typesToDecoders = { auth0: auth0, netlify: netlify, goTrue: netlify, @@ -14,7 +15,7 @@ const typesToDecoders: Record = { firebase: noop, supabase: noop, custom: noop, -} +} as const export const decodeToken = async ( type: SupportedAuthTypes, diff --git a/packages/api/src/auth/index.ts b/packages/api/src/auth/index.ts index 1ebe11922d20..fd988b2b1c61 100644 --- a/packages/api/src/auth/index.ts +++ b/packages/api/src/auth/index.ts @@ -1,16 +1,15 @@ -import type { GlobalContext } from 'src/globalContext' import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda' import type { SupportedAuthTypes } from '@redwoodjs/auth' +import type { GlobalContext } from '../globalContext' + import { decodeToken } from './decoders' // This is shared by `@redwoodjs/web` const AUTH_PROVIDER_HEADER = 'auth-provider' -export const getAuthProviderHeader = ( - event: APIGatewayProxyEvent -): SupportedAuthTypes => { - return event?.headers[AUTH_PROVIDER_HEADER] as SupportedAuthTypes +export const getAuthProviderHeader = (event: APIGatewayProxyEvent) => { + return event?.headers[AUTH_PROVIDER_HEADER] as SupportedAuthTypes | undefined } export interface AuthorizationHeader { @@ -23,7 +22,7 @@ export interface AuthorizationHeader { export const parseAuthorizationHeader = ( event: APIGatewayProxyEvent ): AuthorizationHeader => { - const [schema, token] = event.headers?.authorization?.split(' ') + const [schema, token] = (event.headers?.authorization ?? '').split(' ') if (!schema.length || !token.length) { throw new Error('The `Authorization` header is not valid.') } diff --git a/packages/api/src/functions/fixtures/apiGatewayProxyEvent.fixture.ts b/packages/api/src/functions/fixtures/apiGatewayProxyEvent.fixture.ts index 820c201e8ee5..cf423791a837 100644 --- a/packages/api/src/functions/fixtures/apiGatewayProxyEvent.fixture.ts +++ b/packages/api/src/functions/fixtures/apiGatewayProxyEvent.fixture.ts @@ -1,6 +1,6 @@ import type { APIGatewayProxyEvent } from 'aws-lambda' -export const mockedAPIGatewayProxyEvent: APIGatewayProxyEvent = { +export default { body: 'MOCKED_BODY', headers: {}, multiValueHeaders: {}, @@ -41,6 +41,4 @@ export const mockedAPIGatewayProxyEvent: APIGatewayProxyEvent = { resourcePath: 'MOCKED_RESOURCE_PATH', }, resource: 'MOCKED_RESOURCE', -} - -export default mockedAPIGatewayProxyEvent +} as APIGatewayProxyEvent From 905877ae8776a06192abb1d38e8dfa87734a2e73 Mon Sep 17 00:00:00 2001 From: Krisztiaan Date: Tue, 20 Oct 2020 01:41:15 +0200 Subject: [PATCH 3/3] chore(auth, ts): slightly safer typing for graphql --- .../api/src/functions/authDecoder.test.ts | 19 ++++---- packages/api/src/functions/graphql.ts | 46 ++++++++++--------- .../generate/auth/__tests__/auth.test.js | 19 +++++--- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/api/src/functions/authDecoder.test.ts b/packages/api/src/functions/authDecoder.test.ts index 8060936afa6c..6968ff409601 100644 --- a/packages/api/src/functions/authDecoder.test.ts +++ b/packages/api/src/functions/authDecoder.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import mockedAPIGatewayProxyEvent from './fixtures/apiGatewayProxyEvent.fixture' import * as auth0Decoder from './../auth/decoders/auth0' import * as netlifyDecoder from './../auth/decoders/netlify' @@ -25,7 +26,7 @@ describe('Uses correct Auth decoder', () => { it('handles auth0', async () => { const output = await decodeToken('auth0', MOCKED_JWT, { event: mockedAPIGatewayProxyEvent, - context: {}, + context: {} as any, }) expect(auth0Decoder.auth0).toHaveBeenCalledWith( @@ -41,7 +42,7 @@ describe('Uses correct Auth decoder', () => { it('decodes goTrue with netlify decoder', async () => { const output = await decodeToken('goTrue', MOCKED_JWT, { event: mockedAPIGatewayProxyEvent, - context: {}, + context: {} as any, }) expect(netlifyDecoder.netlify).toHaveBeenCalledWith( @@ -57,7 +58,7 @@ describe('Uses correct Auth decoder', () => { it('decodes netlify with netlify decoder', async () => { const output = await decodeToken('netlify', MOCKED_JWT, { event: mockedAPIGatewayProxyEvent, - context: {}, + context: {} as any, }) expect(netlifyDecoder.netlify).toHaveBeenCalledWith( @@ -73,7 +74,7 @@ describe('Uses correct Auth decoder', () => { it('returns undecoded token for custom', async () => { const output = await decodeToken('custom', MOCKED_JWT, { event: mockedAPIGatewayProxyEvent, - context: {}, + context: {} as any, }) expect(output).toEqual(MOCKED_JWT) @@ -82,7 +83,7 @@ describe('Uses correct Auth decoder', () => { it('returns undecoded token for magicLink', async () => { const output = await decodeToken('magicLink', MOCKED_JWT, { event: mockedAPIGatewayProxyEvent, - context: {}, + context: {} as any, }) expect(output).toEqual(MOCKED_JWT) @@ -91,7 +92,7 @@ describe('Uses correct Auth decoder', () => { it('returns undecoded token for firebase', async () => { const output = await decodeToken('firebase', MOCKED_JWT, { event: mockedAPIGatewayProxyEvent, - context: {}, + context: {} as any, }) expect(output).toEqual(MOCKED_JWT) @@ -100,16 +101,16 @@ describe('Uses correct Auth decoder', () => { it('returns undecoded token for supabase', async () => { const output = await decodeToken('supabase', MOCKED_JWT, { event: mockedAPIGatewayProxyEvent, - context: {}, + context: {} as any, }) expect(output).toEqual(MOCKED_JWT) }) it('returns undecoded token for unknown values', async () => { - const output = await decodeToken('SOMETHING_ELSE!', MOCKED_JWT, { + const output = await decodeToken('SOMETHING_ELSE!' as any, MOCKED_JWT, { event: mockedAPIGatewayProxyEvent, - context: {}, + context: {} as any, }) expect(output).toEqual(MOCKED_JWT) diff --git a/packages/api/src/functions/graphql.ts b/packages/api/src/functions/graphql.ts index 22368e01a565..dbcc6cb71340 100644 --- a/packages/api/src/functions/graphql.ts +++ b/packages/api/src/functions/graphql.ts @@ -1,17 +1,23 @@ import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda' import type { Config, CreateHandlerOptions } from 'apollo-server-lambda' import type { Context, ContextFunction } from 'apollo-server-core' -import type { GlobalContext } from 'src/globalContext' -import type { AuthContextPayload } from 'src/auth' +import type { GlobalContext } from '../globalContext' +import type { AuthContextPayload } from '../auth' +import type { APIGatewayProxyCallback } from 'aws-lambda' import { ApolloServer } from 'apollo-server-lambda' -import { getAuthenticationContext } from 'src/auth' -import { setContext } from 'src/globalContext' +import { getAuthenticationContext } from '../auth' +import { setContext } from '../globalContext' export type GetCurrentUser = ( decoded: AuthContextPayload[0], raw: AuthContextPayload[1] ) => Promise | string> +type ContextProps = { + event: APIGatewayProxyEvent + context: GlobalContext & LambdaContext +} + /** * We use Apollo Server's `context` option as an entry point to construct our * own global context. @@ -22,17 +28,11 @@ export type GetCurrentUser = ( * dataloader instances, and anything else that should be taken into account when * resolving the query. */ -export const createContextHandler = ( - userContext?: Context | ContextFunction, +export function createContextHandler( + userContext?: Context | ContextFunction, getCurrentUser?: GetCurrentUser -) => { - return async ({ - event, - context, - }: { - event: APIGatewayProxyEvent - context: GlobalContext & LambdaContext - }) => { +) { + return async ({ event, context }: ContextProps) => { // Prevent the Serverless function from waiting for all resources (db connections) // to be released before returning a reponse. context.callbackWaitsForEmptyEventLoop = false @@ -46,11 +46,13 @@ export const createContextHandler = ( : authContext } - let customUserContext = userContext - if (typeof userContext === 'function') { - // if userContext is a function, run that and return just the result - customUserContext = await userContext({ event, context }) - } + const customUserContext = + typeof userContext === 'function' + ? await (userContext as ContextFunction)({ + event, + context, + }) + : userContext // Sets the **global** context object, which can be imported with: // import { context } from '@redwoodjs/api' @@ -86,14 +88,14 @@ interface GraphQLHandlerOptions extends Config { * export const handler = createGraphQLHandler({ schema, context, getCurrentUser }) * ``` */ -export const createGraphQLHandler = ({ +export function createGraphQLHandler({ context, getCurrentUser, onException, cors, onHealthCheck, ...options -}: GraphQLHandlerOptions = {}) => { +}: GraphQLHandlerOptions = {}) { const isDevEnv = process.env.NODE_ENV !== 'production' const handler = new ApolloServer({ // Turn off playground, introspection and debug in production. @@ -124,7 +126,7 @@ export const createGraphQLHandler = ({ return ( event: APIGatewayProxyEvent, context: LambdaContext, - callback: any + callback: APIGatewayProxyCallback ): void => { try { handler(event, context, callback) diff --git a/packages/cli/src/commands/generate/auth/__tests__/auth.test.js b/packages/cli/src/commands/generate/auth/__tests__/auth.test.js index fc2ee478cbfc..d1c16788cc48 100644 --- a/packages/cli/src/commands/generate/auth/__tests__/auth.test.js +++ b/packages/cli/src/commands/generate/auth/__tests__/auth.test.js @@ -1,8 +1,6 @@ global.__dirname = __dirname -import { - waitFor, -} from '@testing-library/react' +import { waitFor } from '@testing-library/react' jest.mock('fs') jest.mock('src/lib', () => ({ @@ -17,7 +15,8 @@ import chalk from 'chalk' import * as auth from '../auth' -const EXISTING_AUTH_PROVIDER_ERROR = 'Existing auth provider found.\nUse --force to override existing provider.'; +const EXISTING_AUTH_PROVIDER_ERROR = + 'Existing auth provider found.\nUse --force to override existing provider.' test(`no error thrown when auth provider not found`, async () => { // Mock process.exit to make sure CLI quites @@ -25,7 +24,9 @@ test(`no error thrown when auth provider not found`, async () => { auth.handler({ provider: 'netlify' }) await waitFor(() => expect(console.log).toHaveBeenCalledTimes(1)) - expect(console.log).not.toHaveBeenCalledWith(chalk.bold.red(EXISTING_AUTH_PROVIDER_ERROR)) + expect(console.log).not.toHaveBeenCalledWith( + chalk.bold.red(EXISTING_AUTH_PROVIDER_ERROR) + ) // Restore mocks cSpy.mockRestore() @@ -33,12 +34,16 @@ test(`no error thrown when auth provider not found`, async () => { test('throws an error if auth provider exists', async () => { // Mock process.exit to make sure CLI quites - const fsSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => `import { AuthProvider } from '@redwoodjs/auth'`) + const fsSpy = jest + .spyOn(fs, 'readFileSync') + .mockImplementation(() => `import { AuthProvider } from '@redwoodjs/auth'`) const cSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) auth.handler({ provider: 'netlify' }) await waitFor(() => expect(console.log).toHaveBeenCalledTimes(1)) - expect(console.log).toHaveBeenCalledWith(chalk.bold.red(EXISTING_AUTH_PROVIDER_ERROR)) + expect(console.log).toHaveBeenCalledWith( + chalk.bold.red(EXISTING_AUTH_PROVIDER_ERROR) + ) // Restore mocks fsSpy.mockRestore()