From 617ea6a9c93f040dfda2915e1a79adf360127513 Mon Sep 17 00:00:00 2001 From: Jesus Padron Date: Wed, 29 Nov 2023 16:54:33 -0400 Subject: [PATCH] Feat: handle Auth logic in the handler instead of globally (#41) * fix: handle auth per handler * fix: conditions order in token validation * fix: code cleaness and methods improvements * fix: new api response and add missed token condition --- serverless.yml | 22 ++------------- src/handlers/auth/authorizer.ts | 16 ----------- src/handlers/genres/genreById.ts | 5 ++-- src/handlers/genres/moviesByGenre.ts | 5 ++-- src/handlers/movies/getMovieTitles.ts | 5 ++-- src/handlers/movies/getMovies.ts | 5 ++-- src/handlers/movies/movieById.ts | 5 ++-- src/utils/api/apiAuth.ts | 7 +++-- src/utils/api/apiResponses.ts | 15 ++++++++++ src/utils/api/withAuthorization.ts | 40 +++++++++++++++++++++++++++ 10 files changed, 77 insertions(+), 48 deletions(-) delete mode 100644 src/handlers/auth/authorizer.ts create mode 100644 src/utils/api/withAuthorization.ts diff --git a/serverless.yml b/serverless.yml index a170529..d8f69b8 100644 --- a/serverless.yml +++ b/serverless.yml @@ -36,12 +36,7 @@ provider: CONTENTFUL_DELIVERY_API_TOKEN: ${env:CONTENTFUL_DELIVERY_API_TOKEN} httpApi: cors: true - authorizers: - simpleAuthorizerFunc: - type: request - identitySource: - - $request.header.Authorization - functionName: customAuthorizer + iam: role: statements: @@ -55,10 +50,6 @@ provider: # The `functions` block defines what code to deploy functions: - # auth functions & APIs - customAuthorizer: - handler: src/handlers/auth/authorizer.handler - getValidToken: handler: src/handlers/auth/getValidToken.handler events: @@ -94,8 +85,6 @@ functions: - httpApi: path: /genres/movies method: get - authorizer: - name: simpleAuthorizerFunc movies: handler: src/handlers/movies/getMovies.handler @@ -103,8 +92,6 @@ functions: - httpApi: path: /movies method: get - authorizer: - name: simpleAuthorizerFunc movieById: handler: src/handlers/movies/movieById.handler @@ -112,8 +99,6 @@ functions: - httpApi: path: /movies/{id} method: get - authorizer: - name: simpleAuthorizerFunc movieTitles: handler: src/handlers/movies/getMovieTitles.handler @@ -121,8 +106,6 @@ functions: - httpApi: path: /movies/titles method: get - authorizer: - name: simpleAuthorizerFunc genreById: handler: src/handlers/genres/genreById.handler @@ -130,5 +113,4 @@ functions: - httpApi: path: /movies/genres/{id} method: get - authorizer: - name: simpleAuthorizerFunc + diff --git a/src/handlers/auth/authorizer.ts b/src/handlers/auth/authorizer.ts deleted file mode 100644 index 9e947e1..0000000 --- a/src/handlers/auth/authorizer.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { isTokenValid } from '@utils/api/apiAuth'; -import { APIGatewayRequestSimpleAuthorizerHandlerV2 } from 'aws-lambda'; - -export const handler: APIGatewayRequestSimpleAuthorizerHandlerV2 = async (event) => { - const authorization = event.headers?.['authorization'] || ''; - - // removing the initial "Bearer " - const token = authorization.substring(7); - const isAuthorized = isTokenValid(token); - - // ref: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.payload-format-response - return { - isAuthorized: isAuthorized, - context: {}, - }; -}; diff --git a/src/handlers/genres/genreById.ts b/src/handlers/genres/genreById.ts index 040808e..2b7fbef 100644 --- a/src/handlers/genres/genreById.ts +++ b/src/handlers/genres/genreById.ts @@ -6,8 +6,9 @@ import { notFoundResponse, serverErrorResponse, } from '@utils/api/apiResponses'; +import { withAuthorization } from '@utils/api/withAuthorization'; -export const handler: APIGatewayProxyHandler = async (event) => { +export const handler: APIGatewayProxyHandler = withAuthorization(async (event) => { const genreId = event?.pathParameters?.id; if (!genreId) { @@ -33,4 +34,4 @@ export const handler: APIGatewayProxyHandler = async (event) => { } catch (err) { return serverErrorResponse; } -}; +}); diff --git a/src/handlers/genres/moviesByGenre.ts b/src/handlers/genres/moviesByGenre.ts index 61a0af5..2eb159e 100644 --- a/src/handlers/genres/moviesByGenre.ts +++ b/src/handlers/genres/moviesByGenre.ts @@ -2,8 +2,9 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; import getAllGenre from '@models/Genre/getAll'; import { mountSuccessResponse, serverErrorResponse } from '@utils/api/apiResponses'; import { DEFAULT_CONTENTFUL_LIMIT } from '@utils/contentful'; +import { withAuthorization } from '@utils/api/withAuthorization'; -export const handler: APIGatewayProxyHandler = async (event) => { +export const handler: APIGatewayProxyHandler = withAuthorization(async (event) => { const { page: queryStringPage = '', limit: queryStringLimit = '' } = event?.queryStringParameters || {}; @@ -25,4 +26,4 @@ export const handler: APIGatewayProxyHandler = async (event) => { } catch (err) { return serverErrorResponse; } -}; +}); diff --git a/src/handlers/movies/getMovieTitles.ts b/src/handlers/movies/getMovieTitles.ts index 02ec310..5907881 100644 --- a/src/handlers/movies/getMovieTitles.ts +++ b/src/handlers/movies/getMovieTitles.ts @@ -2,8 +2,9 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; import getAllMovies from '@models/Movie/getAll'; import { mountSuccessResponse, serverErrorResponse } from '@utils/api/apiResponses'; import { DEFAULT_CONTENTFUL_LIMIT } from '@utils/contentful'; +import { withAuthorization } from '@utils/api/withAuthorization'; -export const handler: APIGatewayProxyHandler = async (event) => { +export const handler: APIGatewayProxyHandler = withAuthorization(async (event) => { const { page: queryStringPage = '', limit: queryStringLimit = '' } = event?.queryStringParameters || {}; @@ -25,4 +26,4 @@ export const handler: APIGatewayProxyHandler = async (event) => { } catch (err) { return serverErrorResponse; } -}; +}); diff --git a/src/handlers/movies/getMovies.ts b/src/handlers/movies/getMovies.ts index a95a1ee..ee493da 100644 --- a/src/handlers/movies/getMovies.ts +++ b/src/handlers/movies/getMovies.ts @@ -10,6 +10,7 @@ import { notFoundResponse, serverErrorResponse, } from '@utils/api/apiResponses'; +import { withAuthorization } from '@utils/api/withAuthorization'; type SearchFilters = { page?: number; @@ -19,7 +20,7 @@ type SearchFilters = { include?: ContentfulIncludeOptions; }; -export const handler: APIGatewayProxyHandler = async (event) => { +export const handler: APIGatewayProxyHandler = withAuthorization(async (event) => { try { const searchFilters: { page?: number; @@ -72,7 +73,7 @@ export const handler: APIGatewayProxyHandler = async (event) => { console.error('Error fetching movies:', error); return serverErrorResponse; } -}; +}); export async function fetchMoviesByGenre(searchFilters: SearchFilters) { if (!searchFilters.genre) { diff --git a/src/handlers/movies/movieById.ts b/src/handlers/movies/movieById.ts index adcba5e..1f6b0ff 100644 --- a/src/handlers/movies/movieById.ts +++ b/src/handlers/movies/movieById.ts @@ -7,8 +7,9 @@ import { import getMovieById from '@models/Movie/getById'; import { CONTENTFUL_INCLUDE } from '@customTypes/contentful'; import { isCustomContentfulError } from '@utils/api/utils'; +import { withAuthorization } from '@utils/api/withAuthorization'; -export const handler: APIGatewayProxyHandler = async (event) => { +export const handler: APIGatewayProxyHandler = withAuthorization(async (event) => { const movieId = event?.pathParameters?.id; if (!movieId) { @@ -38,4 +39,4 @@ export const handler: APIGatewayProxyHandler = async (event) => { return serverErrorResponse; } -}; +}); diff --git a/src/utils/api/apiAuth.ts b/src/utils/api/apiAuth.ts index 36bb99a..c45fa1d 100644 --- a/src/utils/api/apiAuth.ts +++ b/src/utils/api/apiAuth.ts @@ -1,4 +1,5 @@ // Genereated from https://jwt.io/ + export const authorizedJWTs = [ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJvcGVuSnd0MCIsIm5hbWUiOiJPcGVuSldUWzBdIn0.49JQF4ICJeqxpiIZ9x748VVOHj6FElyRm1tNpFGqaUY', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJvcGVuSnd0MSIsIm5hbWUiOiJPcGVuSldUWzFdIn0.n8x8GHYe8RQYKkAoMVMlw9-FMZ57bs0HrwxBeJn3hQM', @@ -8,10 +9,12 @@ export const authorizedJWTs = [ ]; export function isTokenValid(token: string) { - if (!token) { + if (!token || !token.startsWith('Bearer ')) { return false; } - return authorizedJWTs.includes(token); + const tokenValue = token.substring(7); + + return authorizedJWTs.includes(tokenValue); } export function getRandomValidToken(): string { diff --git a/src/utils/api/apiResponses.ts b/src/utils/api/apiResponses.ts index cd54aff..dd5c89f 100644 --- a/src/utils/api/apiResponses.ts +++ b/src/utils/api/apiResponses.ts @@ -20,3 +20,18 @@ export const mountSuccessResponse = (bodyObject: object): APIGatewayProxyResult body: JSON.stringify(bodyObject), }; }; + +export const unauthorizedResponse: APIGatewayProxyResult = { + statusCode: 401, + body: JSON.stringify({ message: 'No auth token provided.' }), +}; + +export const forbiddenResponse: APIGatewayProxyResult = { + statusCode: 403, + body: JSON.stringify({ message: 'You do not have permission to access this resource' }), +}; + +export const noContentResponse: APIGatewayProxyResult = { + statusCode: 204, + body: JSON.stringify({ message: 'No content' }), +}; diff --git a/src/utils/api/withAuthorization.ts b/src/utils/api/withAuthorization.ts new file mode 100644 index 0000000..d86da2f --- /dev/null +++ b/src/utils/api/withAuthorization.ts @@ -0,0 +1,40 @@ +import { + APIGatewayProxyEvent, + APIGatewayProxyHandler, + APIGatewayProxyResult, + Callback, + Context, +} from 'aws-lambda'; +import { isTokenValid } from '@utils/api/apiAuth'; +import { + forbiddenResponse, + noContentResponse, + unauthorizedResponse, +} from '@utils/api/apiResponses'; + +export function withAuthorization(handler: APIGatewayProxyHandler): APIGatewayProxyHandler { + return async ( + event: APIGatewayProxyEvent, + context: Context, + callback: Callback + ): Promise => { + const authorization = event.headers?.['authorization'] || ''; + + if (!authorization) { + return unauthorizedResponse; + } + + if (!isTokenValid(authorization)) { + return forbiddenResponse; + } + + const result = await handler(event, context, callback); + + // Provide a default response if the handler returns void + if (result === undefined) { + return noContentResponse; + } + + return result; + }; +}