Firebase cloud functions Appcheck for https.onRequest

Viewed 1481

As per documentation we can add appcheck as below,

exports.yourCallableFunction = functions.https.onCall((data, context) => {
  // context.app will be undefined if the request doesn't include a valid
  // App Check token.
  if (context.app == undefined) {
    throw new functions.https.HttpsError(
        'failed-precondition',
        'The function must be called from an App Check verified app.')
  }
});

My question right now is how do I need to add app-check for below scenario?

exports.date = functions.https.onRequest((req, res) => {

});
4 Answers

In the client, get an appCheck token from Firebase. Send it in a header to your function. Get the token from the req object's headers. Verify the the token with firebase-admin. I'll include the documentation for the client below, then the gist of how I implemented it client side with Apollo-client graphql. Then I'll include the documentation for the backend, then the gist of how I implemented the backend, again with Apollo.

client (from the documentation):

const { initializeAppCheck, getToken } = require('firebase/app-check');

const appCheck = initializeAppCheck(
    app,
    { provider: provider } // ReCaptchaV3Provider or CustomProvider
);

const callApiWithAppCheckExample = async () => {
  let appCheckTokenResponse;
  try {
      appCheckTokenResponse = await getToken(appCheck, /* forceRefresh= */ false);
  } catch (err) {
      // Handle any errors if the token was not retrieved.
      return;
  }

  // Include the App Check token with requests to your server.
  const apiResponse = await fetch('https://yourbackend.example.com/yourApiEndpoint', {
      headers: {
          'X-Firebase-AppCheck': appCheckTokenResponse.token,
      }
  });

  // Handle response from your backend.
}; 

client (gist from my implementation)

import { setContext } from "@apollo/client/link/context";
import { app } from '../firebase/setup';
import { initializeAppCheck, ReCaptchaV3Provider, getToken } from "firebase/app-check"

let appCheck
let appCheckTokenResponse

const getAppCheckToken = async () => {
  const appCheckTokenResponsePromise = await getToken(appCheck, /* forceRefresh= */ false)
  appCheckTokenResponse = appCheckTokenResponsePromise
}

const authLink = setContext(async (_, { headers }) => {
  if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_ENV === 'production') {
    appCheck = initializeAppCheck(app, {
      provider: new ReCaptchaV3Provider('my_public_key_from_recaptcha_V3'),
      isTokenAutoRefreshEnabled: true
    })
    await getAppCheckToken()
  }

  return {
    headers: {
      ...headers,
      'X-Firebase-AppCheck': appCheckTokenResponse?.token,
    },
  }
})

backend / server (from the documentation)

const express = require('express');
const app = express();

const firebaseAdmin = require('firebase-admin');
const firebaseApp = firebaseAdmin.initializeApp();

const appCheckVerification = async (req, res, next) => {
    const appCheckToken = req.header('X-Firebase-AppCheck');

    if (!appCheckToken) {
        res.status(401);
        return next('Unauthorized');
    }

    try {
        const appCheckClaims = await firebaseAdmin.appCheck().verifyToken(appCheckToken);

        // If verifyToken() succeeds, continue with the next middleware
        // function in the stack.
        return next();
    } catch (err) {
        res.status(401);
        return next('Unauthorized');
    }
}

app.get('/yourApiEndpoint', [appCheckVerification], (req, res) => {
    // Handle request.
});

backend / server (gist from my implementation)

import { https } from 'firebase-functions'
import gqlServer from './graphql/server'
const functions = require('firebase-functions')

const env = process.env.ENV || functions.config().config.env

const server = gqlServer()

const api = https.onRequest((req, res) => {
    server(req, res)
})

export { api }

. . .


import * as admin from 'firebase-admin';
const functions = require('firebase-functions');

const env = process.env.ENV || functions.config().config.env

admin.initializeApp()


appCheckVerification = async (req: any, res: any) => {
  const appCheckToken = req.header('X-Firebase-AppCheck')
  if (!appCheckToken) {
    return false
  }

  try {
    const appCheckClaims = await admin.appCheck().verifyToken(appCheckToken);
    return true
  } catch (error) {
    console.error(error)
    return false
  }
 }

. . .


const apolloServer = new ApolloServer({
  introspection: isDevelopment,
  typeDefs: schema,
  resolvers,
  context: async ({ req, res }) => {
            
    if (!isDevelopment && !isTest) {
      const appCheckVerification = await appCheckVerification(req, res)
        if (!appCheckVerification) throw Error('Something went wrong with verification')
 }
return { req, res, }
}

If you enforce app check in Cloud Functions it will only allow calls from apps that are registered in your project.

I'm not sure if that is sufficient for your use-case though, as I doubt most apps where you can provide a web hook will have implemented app attestation - which is how App Check recognizes valid requests.

You can generate an app check token in the client and verify the token in the server using firebase admin SDK. Here is the firebase documentation for the same

Firebase enable App check enforcement documentation teaches you that to validate the caller from your function you just need to check the context.app then gives you an example like this

exports.EXAMPLE = functions.https.onCall((data, context) => {});

https://firebase.google.com/docs/app-check/cloud-functions?authuser=0

But when you are deploying your function in the google cloud dashboard, you select HTTP FUNCTION -> nodejs 14 -> then you are directed to code like this

/**
 * Responds to any HTTP request.
 *
 * @param {!express:Request} req HTTP request context.
 * @param {!express:Response} res HTTP response context.
 */
exports.helloWorld = (req, res) => {
  let message = req.query.message || req.body.message || 'Hello World!';
  res.status(200).send(message);
};

My question when I saw this was: "How am i going to get a context if I only have request/response"

The answer is simple. YOU MUST SWITCH THE CONSTRUCTORS

You must re-write your function in a way that instead of dealing with req/res like any express function you are dealing with context/data

http functions are different of callable functions (the ones that deals with context/data)

IT IS SIMILAR BUT NOT EXACTLY EQUAL AND SOME MODIFICATIONS WILL BE NECESSARY.

mainly if your function deals with async stuff and have a delayed response you are going to need to rewrite many stuff

check this tutorial https://firebase.google.com/docs/functions/callable

Related