5

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?

Johnny Oshika
  • 54,741
  • 40
  • 181
  • 275

2 Answers2

11

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
    },
  ),
);
Johnny Oshika
  • 54,741
  • 40
  • 181
  • 275
  • 1
    thanks @Johnny Oshika. Nice inspiring design. It's a pure lean vanilla solution and still concise, I like that. Moreover, a similar design can be implemented for `onRequest` functions. – Xavier D Sep 13 '22 at 07:04
  • 1
    I've created a gist for this, replacing `any` with `unknown` and adding missing types so it now passes the TypeScript lint checker. Not tested! https://gist.github.com/tohagan/13822a52ce48b59e0a41ba67af1d93ac – Tony O'Hagan Oct 22 '22 at 07:01
  • @TonyO'Hagan Excellent! The switch to `unknown` is a good idea. – Johnny Oshika Oct 25 '22 at 03:33
0

Middleware for Firebase callable functions is not possible. Callable functions force your endpoint to use a certain path, a certain type of input (JSON via POST) and a certain type of output (also JSON). Express wouldn't really help you out, given the constraints of how callables work. You can read about all the callable protocol details in the documentation. You can see that callables abstract away all the details of the request and response, which you would normally work with when using Express.

As per this community answer,

HTTP requests to callable functions don't really come "from" a URL. They come from anywhere on the internet. It could be a web site, Android or iOS app, or someone who simply knows the protocol to call the function. If you're building a web app and you want to pass along the URL of the page making the request, you'll have to add that data into the object that the client passes to the function, which shows up in data.

So unless you workaround that by sending the URL in the data of the callable function, it will not work. And even if you do, it just would go against the principle of callable functions, so I would recommend that you use HTTP Functions for that purpose.

Priyashree Bhadra
  • 3,182
  • 6
  • 23
  • 3
    You can unwrap the raw request of a Callable HTTPS Function using [`context.rawRequest`](https://firebase.google.com/docs/reference/functions/common_providers_https.callablecontext#rawrequest) if you need things like the URL or the remote caller's IP address. – samthecodingman Nov 19 '21 at 11:48
  • 1
    It seems that your answer is specific to routing within middlewares. This question is more general than that. While middlewares (like the one used by express) can offer routing, they're not exclusively for that. There are many use cases for middlewares without routing (see examples in my question). I also provided an example solution as an answer below. Please let me know what you think. – Johnny Oshika Nov 21 '21 at 19:11