@senacor/azure-function-middleware
Version:
Middleware for azure functions to handle authentication, authorization, error handling and logging
294 lines (223 loc) • 11.2 kB
Markdown
# 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 { AppInsightForHttpTrigger, 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(
[AppInsightForHttpTrigger.setup, requestBodyValidation(requestBodySchema)],
httpHandler,
[AppInsightForHttpTrigger.finalize]
),
});
```
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)`.
### Logging and Tracing with Application Insights
To enhance the logging and tracing with appInsights you can wrap your function with the appInsightWrapper.
Currently, this will add request parameters and workflow data into the customProperties, which will make your logs more searchable.
Use the `AppInsightForHttpTrigger` for your http-functions:
```typescript
import { HttpHandler, app } from '@azure/functions';
import { AppInsightForHttpTrigger, 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([AppInsightForHttpTrigger.setup], httpHandler, [AppInsightForHttpTrigger.finalize]),
});
```
and the `AppInsightForNoNHttpTrigger` for functions with different kinds of trigger (e.g. `activityTrigger` or `timerTrigger`).
Per default the request and response bodies of http requests are only logged if the request fails. You can customize this
behavior by using `AppInsightForHttpTrigger.finalizeWithConfig(...)` instead of `AppInsightForHttpTrigger.finalizeAppInsight`.
There you can also provide a function to sanitize the request and response bodies to prevent logging of sensitive data.
## 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>.