Firebase cloud functions Appcheck for https.onRequest
Asked Answered
L

5

8

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) => {

});
Lorou answered 22/6, 2021 at 12:7 Comment(0)
R
7

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, }
}
Romain answered 6/9, 2021 at 20:59 Comment(0)
S
1

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

Shaped answered 16/10, 2021 at 15:29 Comment(0)
Q
0

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.

Quinton answered 22/6, 2021 at 14:39 Comment(3)
Does denied calls also cost ??Sara
@Sara SURE, google is NEVER going to let you run out of charge... the only advantage of this it will help you to avoid a long complex database query or something like that, but the function call, memory and data transfer costs STILL APPLYShaped
Don't you need to add the app check token in some header on the client ?Malcommalcontent
M
0

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

Marley answered 11/8, 2021 at 6:46 Comment(1)
Could you provide an example from the documentation? This will help future users if the URL becomes invalid.Doleful
W
0

Client

  1. After deploying the function, set the function-URL in the client.
  2. Set the AppCheck token in the client.

Examples can be found in the docs, here is the one for Flutter:

final appCheckToken = await FirebaseAppCheck.instance.getToken();

if (appCheckToken != null) {
  final response = await http.get(

    Uri.parse("https://yourbackend.example.com/yourExampleEndpoint"),
    headers: {"X-Firebase-AppCheck": appCheckToken},

   );
} else {
  // Error: couldn't get an App Check token.
}

Server

When deploying the function, the console will print the resulting function-URL. Use this URL in the client's code.

  1. Set Cors to enable all incoming calls (depends on your project).
  2. Verify the incoming AppCheck-Token.
  3. Depending on verification result, return 401 or process the request normally.
const cors = require("cors")({origin: true});

exports.createBundle = functions.region("europe-west3")
    .https.onRequest(async (request, response) => {
      cors(request, response, async () => {
        const appCheckToken = request.header("X-Firebase-AppCheck");
        if (!appCheckToken) {
          response.statusMessage = "Unauthorized";
          response.statusCode = 401;
          response.end();
        }

        try {
          await admin.appCheck().verifyToken(appCheckToken);
        } catch (err) {
          response.statusMessage = "Unauthorized";
          response.statusCode = 401;
          response.end();
        }

        const payload = "your response payload";

        // see https://firebase.google.com/docs/hosting/manage-cache
        response.set("Cache-Control", "public, max-age=86400, s-maxage=86400");
        response.statusCode = 200;
        response.end(payload);
      });
    });
Wheelhouse answered 18/4, 2023 at 17:41 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.