UNPKG

@senacor/azure-function-middleware

Version:

Middleware for azure functions to handle authentication, authorization, error handling and logging

264 lines (201 loc) 9.78 kB
# Azure Function Middleware The Azure Function Middleware introduces a middleware pattern for [Azure Functions in Node.js](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview?pivots=programming-language-javascript), enhancing the development experience by simplifying the integration of cross-cutting concerns such as schema validation, authorization, and error handling. ## Installation Before you integrate this middleware into your project, ensure you have Node.js installed, and you're familiar with Azure Functions. Follow these steps to set up: ```bash npm install @senacor/azure-function-middleware ``` ## Usage The middleware interface is intuitive, designed for expansion, and integrates seamlessly with Azure Functions. Here's a quick example to get you started: ```typescript import { HttpRequest, InvocationContext, app } from '@azure/functions'; import { middleware, requestBodyValidation } from '@senacor/azure-function-middleware'; import * as Joi from 'joi'; const httpHandler = async (request: HttpRequest, context: InvocationContext) => { context.info('function called'); return { status: 201 }; }; const requestBodySchema = Joi.object().keys({ name: Joi.string().min(3).max(30).required(), }); app.http('example-function', { methods: ['POST'], authLevel: 'anonymous', route: 'example', handler: middleware( [requestBodyValidation(requestBodySchema)], httpHandler, [] ), }); ``` This pattern aims to deliver a core set of features and a simplified interface for creating additional middleware functions tailored for Azure Functions. ## Error Handling Centralized error management is a key feature, ensuring all errors within the function's flow are intercepted and appropriately handled. Specific error responses can be defined by throwing errors in the following format: ```typescript export class ApplicationError<T> extends Error { status: number; body?: T; } ``` Any error thrown in the function with this signature is getting returned to the caller with the defined status and body. ## Generic Functions The middleware supports the integration of generic functions like request validation or authorization. These functions must comply with the 'AzureFunction' type from the '@azure/functions' package. They are crucial for extending the middleware's capabilities while adhering to Azure's function signature requirements. Here is an example how you can write your own before and post execution functions: ```typescript import { HttpHandler, app } from '@azure/functions'; import { BeforeExecutionFunction, PostExecutionFunction, middleware } from '@senacor/azure-function-middleware'; export const httpHandler: HttpHandler = async (request, context) => { context.info('Function called'); return { status: 201 }; }; const beforeFunction: BeforeExecutionFunction = (request, context, result) => { context.info('Called before httpHandler'); }; const postFunction: PostExecutionFunction = (request, context, result) => { context.log('Called after httpHandler'); }; app.http('test-validation-function', { methods: ['POST'], authLevel: 'anonymous', route: 'validation', handler: middleware([beforeFunction], httpHandler, [postFunction]), }); ``` First all `beforeExecution` functions are executed in the given order. Then the `httpHandler` is called and afterwards all `postExecution` are executed in the given order. The Azure Function Middleware provides some useful before and post execution functions which are described in the following sections. ### Request Body Validation The function validates the request body based on a [Joi Schema](https://www.npmjs.com/package/joi). ```typescript import { HttpHandler, app } from '@azure/functions'; import { middleware, requestBodyValidation } from '@senacor/azure-function-middleware'; import { ObjectSchema } from 'joi'; import * as Joi from 'joi'; export const httpHandler: HttpHandler = async (req, context) => { context.info('Function called'); return { status: 201 }; }; const requestBodySchema: ObjectSchema = Joi.object({ name: Joi.string().min(3).max(30).required(), }).required(); app.http('example-function', { methods: ['POST'], authLevel: 'anonymous', route: 'example', handler: middleware([requestBodyValidation(requestBodySchema)], httpHandler, []), }); ``` By default, an `ApplicationError` with status 400 will be thrown and the `httpHandler` is not executed if the request body does not match the provided schema. There is an additional parameter to customize the behavior of `requestBodyValidation` (see [requestBodyValidation.ts](src/validation/requestBodyValidation.ts)). ### Request Query Params Validation The function validates the request query parameters based on a [Joi Schema](https://www.npmjs.com/package/joi). ```typescript import { HttpHandler, app } from '@azure/functions'; import { middleware, requestQueryParamsValidation } from '@senacor/azure-function-middleware'; import { ObjectSchema } from 'joi'; import * as Joi from 'joi'; export const httpHandler: HttpHandler = async (req, context) => { context.info('Function called'); return { status: 201 }; }; const queryParamsSchema: ObjectSchema = Joi.object({ name: Joi.string().valid('active', 'expired').optional(), }); app.http('example-function', { methods: ['POST'], authLevel: 'anonymous', route: 'example', handler: middleware([requestQueryParamsValidation(queryParamsSchema)], httpHandler, []), }); ``` By default, an `ApplicationError` with status 400 will be thrown and the `httpHandler` is not executed if the query params do not match the provided schema. There is an additional parameter to customize the behavior of `requestQueryParamsValidation` (see [requestQueryParamsValidation.ts](src/validation/requestQueryParamsValidation.ts)). ### Response Body Validation The function validates the response body based on a [Joi Schema](https://www.npmjs.com/package/joi). ```typescript import { HttpHandler, app } from '@azure/functions'; import { middleware, responseBodyValidation } from '@senacor/azure-function-middleware'; import { ObjectSchema } from 'joi'; import * as Joi from 'joi'; export const httpHandler: HttpHandler = async (req, context) => { context.info('Function called'); return { status: 200, jsonBody: { name: 'John Doe', }, }; }; const responseSchema: ObjectSchema = Joi.object({ name: Joi.string().required(), }); app.http('example-function', { methods: ['GET'], authLevel: 'anonymous', route: 'example', handler: middleware([], httpHandler, [responseBodyValidation(responseSchema)]), }); ``` By default, an error is logged if the response body does not match the provided schema. There is an additional parameter to customize the behavior of `responseBodyValidation` (see [responseBodyValidation.ts](src/validation/responseBodyValidation.ts)). ### Authorization The authorization function verifies request parameters against JWT Bearer Tokens, employing customizable extraction functions for flexible security checks. **IMPORTANT**: The signature of the JWT is not validated. Any well-formed JWT can be decoded (see [jwt-decode](https://www.npmjs.com/package/jwt-decode)). ```typescript import { HttpHandler, HttpRequestParams, app } from '@azure/functions'; import { jwtAuthorization, middleware } from '@senacor/azure-function-middleware'; export const handler: HttpHandler = async (req, context) => { context.log(`Function called by ${context.extraInputs.get('jwt')}`); return { status: 204 }; }; app.http('test-jwt-authorization-function', { methods: ['POST'], authLevel: 'anonymous', route: 'account/{accountId}', handler: middleware<HttpHandler>( [ jwtAuthorization([ { parameterExtractor: (parameters: HttpRequestParams) => parameters.accountId, jwtExtractor: (jwt: { sub: string }) => jwt.sub, }, ]), ], handler, [], ), }); ``` The passed values in the array needs to be defined based on the following structure: ```typescript export type Rule<T> = { parameterExtractor: (parameters: HttpRequestParams) => string; jwtExtractor: (jwt: T) => string; }; ``` ### Header Authentication To authenticate requests against a rule, the header could be used. Therefore, the `headerAuthentication` pre-function is available. ```typescript import { HttpHandler, app } from '@azure/functions'; import { headerAuthentication, middleware } from '@senacor/azure-function-middleware'; export const httpHandler: HttpHandler = async (req, context) => { context.info('Function called'); return { status: 201 }; }; app.http('example-function', { methods: ['POST'], authLevel: 'anonymous', route: 'example', handler: middleware([headerAuthentication()], httpHandler, []), }); ``` When no parameter is passed to the `headerAuthentication` the header `x-ms-client-principal-id` is checked, if present or not. This header is added to a request by the Azure plattform when e.g. a JWT Token is successfully validated. The `x-ms-client-principal-id` and `x-ms-client-principal-name` header could only be set by the Azure plattform (https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-user-identities). It is also possible to pass a function to validate a specific header, like checking for basic authentication credentials. This could be done in the following manner `headerAuthentication((headers: Headers) => boolean)`. ## Support and Contact If you encounter any issues or have questions about using this middleware, please file an issue in this repository or contact the maintainers at <florian.rudisch@senacor.com> or <manuel.kniep@senacor.com>.