简体   繁体   中英

Middleware for Firebase Callable Functions

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?

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.

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 easy to roll my own.

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 () => {
    // 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
    },
  ),
);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM