With Firebase HTTP functions, we can install express and use middlewares. Middlewares are useful (among other things) for checking pre-conditions before functions execute. For example, we can check authentication, authorization, etc in middlewares so that they don't need to be repeated in every endpoint definition.
How are developers achieving the same thing with Firebase callable functions? How are you extracting out all functionality that would typically be in chained middlewares when you have a large number of callable functions?
It seems that there's no readily available middleware framework for callable functions, so inspired by this, I rolled my own. There are some general purpose chained middleware frameworks on NPM, but the middleware I need is so simple that it was easier to roll my own than to configure a library to work with callable functions.
Optional: Type declaration for Middleware if you're using TypeScript:
export type Middleware = (
data: any,
context: functions.https.CallableContext,
next: (
data: any,
context: functions.https.CallableContext,
) => Promise<any>,
) => Promise<any>;
Here's the middleware framework:
export const withMiddlewares = (
middlewares: Middleware[],
handler: Handler,
) => (data: any, context: functions.https.CallableContext) => {
const chainMiddlewares = ([
firstMiddleware,
...restOfMiddlewares
]: Middleware[]) => {
if (firstMiddleware)
return (
data: any,
context: functions.https.CallableContext,
): Promise<any> => {
try {
return firstMiddleware(
data,
context,
chainMiddlewares(restOfMiddlewares),
);
} catch (error) {
return Promise.reject(error);
}
};
return handler;
};
return chainMiddlewares(middlewares)(data, context);
};
To use it, you would attach withMiddlewares
to any callable function. For example:
export const myCallableFunction = functions.https.onCall(
withMiddlewares([assertAppCheck, assertAuthenticated], async (data, context) => {
// Your callable function handler
}),
);
There are 2 middlewares used in the above example. They are chained so assertAppCheck
is called first, then assertAuthenticated
, and only after they both pass does your hander get called.
The 2 middleware are:
assertAppCheck:
/**
* Ensures request passes App Check
*/
const assertAppCheck: Middleware = (data, context, next) => {
if (context.app === undefined)
throw new HttpsError('failed-precondition', 'Failed App Check.');
return next(data, context);
};
export default assertAppCheck;
assertAuthenticated:
/**
* Ensures user is authenticated
*/
const assertAuthenticated: Middleware = (data, context, next) => {
if (!context.auth?.uid)
throw new HttpsError('unauthenticated', 'Unauthorized.');
return next(data, context);
};
export default assertAuthenticated;
As a bonus, here's a validation middleware that uses Joi to ensure the data is validated before your handler gets called:
const validateData: (schema: Joi.ObjectSchema<any>) => Middleware = (
schema: Joi.ObjectSchema<any>,
) => {
return (data, context, next) => {
const validation = schema.validate(data);
if (validation.error)
throw new HttpsError(
'invalid-argument',
validation.error.message,
);
return next(data, context);
};
};
export default validateData;
Use the validation middleware like this:
export const myCallableFunction = functions.https.onCall(
withMiddlewares(
[
assertAuthenticated,
validateData(
Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
}),
),
],
async (data, context) => {
// Your handler
},
),
);
2024-01-05 update for gen2
Here is the code adapted for gen2
functions:
import { CallableRequest } from 'firebase-functions/v2/https';
export type Handler<T = any, Return = any> = (
request: CallableRequest<T>,
) => Promise<Return>;
export type Middleware<T = any, Return = any> = (
request: CallableRequest<T>,
next: (request: CallableRequest<T>) => Promise<Return>,
) => Promise<Return>;
export const withMiddlewares =
<T = any, Return = any>(
middlewares: Middleware<T, Return>[],
handler: Handler<T, Return>,
) =>
(request: CallableRequest<T>): Promise<Return> => {
const chainMiddlewares = ([
firstMiddleware,
...restOfMiddlewares
]: Middleware<T, Return>[]): Handler<T, Return> => {
if (firstMiddleware)
return (request: CallableRequest<T>): Promise<Return> =>
firstMiddleware(
request,
chainMiddlewares(restOfMiddlewares),
);
else return handler;
};
return chainMiddlewares(middlewares)(request);
};