A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/firebase/firebase-js-sdk/discussions/5095 below:

Web AppCheck is not GDPR compliant · firebase/firebase-js-sdk · Discussion #5095 · GitHub

I filed a support ticket about this a while ago but I wanted to open a wider discussion on this topic in case other developers are unaware of this issue.

Because App Check in web apps uses ReCAPTCHA v3 by default, it is not possible for a web app to use App Check and be GDPR compliant without first replacing ReCAPTCHA with your own custom attestation provider. There are several sources where you can find more specific information about the problems with ReCAPTCHA and GDPR, but the main issues are these:

  1. reCAPTCHA v3 utilizes several cookies that collect personal information about site visitors.
  2. A website must get consent from the user before these cookies are installed. While it is possible to delay App Check's activation until after you obtain consent, it defeats the purpose of App Check entirely if a malicious user can bypass it by simply opting out.
  3. If you decide to gate access to your website's core functionality behind AppCheck, (so that the user cannot use features of your site without first agreeing to let your app install reCAPTCHA's tracking cookies), this also violates GDPR. According to GDPR, the core functionality of a website cannot be gated behind non-optional consent (aka an illegal "cookie wall")

These are just some of the legal problems we found when researching GDPR compliance and reCAPTCHA. Also these issues may not be apparent to devs who are currently utilizing AppCheck in their websites.

If you'd like to use AppCheck in your web app and remain GDPR compliant, you'll need to create a custom AppCheckProvider that does not use reCAPTCHA. Firebase's current documentation is fairly vague about how to do this, so here's a step-by-step guide of one possible way to use AppCheck and still comply with GDPR (Disclaimer: Not a lawyer. So please make sure to check your implementation with your own legal advisor):

  1. Create a custom AppCheckProvider.

This is an example of what a custom app check provider may look like (in typescript)

import { AppCheck, initializeAppCheck, CustomProvider } from "@firebase/app-check";
import FirebaseClient from "../../lib/client";

export const getToken = async (): Promise<{
  readonly token: string;
  readonly expireTimeMillis: number;
}> => {
  Logger.instance.info(LogSource.API, "UPDATE APP CHECK TOKEN");
  try {
    const idToken = await FirebaseClient.auth().currentUser.getIdToken(true);
    const response = await fetch("/api/verifyAppCheck", {
      credentials: "include",
      method: "POST",
      body: JSON.stringify({
        token: idToken,
      }),
      headers: { "Content-Type": "application/json" },
    });
    const result = await response.json();
    if (!response.ok) {
      throw new Error(result.message);
    }
    return result;
  } catch (error) {
    console.error(error);
  }
  return undefined;
};

export class AppCheckVerifier {
  private static _instance: AppCheckVerifier;

  public static get instance(): AppCheckVerifier {
    if (!this._instance) {
      this._instance = new AppCheckVerifier();
    }
    return this._instance;
  }

  private _appCheck: AppCheck;

  verify(): void {
    if (!this._appCheck) {
      this._appCheck = initializeAppCheck(this.app, {
        provider: new CustomProvider({ getToken }),
      });
    }
  }
  
  async verifyCaptchaClaim(captcha: string): Promise<UserClaims> {
    try {
      const token = await FirebaseClient.auth().currentUser.getIdToken(true);
      const response = await fetch("/api/verifyCaptchaClaim", {
        method: "POST",
        body: JSON.stringify({ token, captcha }),
        headers: { "Content-Type": "application/json" },
      });
      const result = (await response.json()) as {
        message?: string;
      };
      if (!response.ok) {
        throw new Error(result.message);
      }
      const token = await FirebaseClient.auth().currentUser
        .getIdTokenResult(true)
      Logger.instance.info(LogSource.API, "VERIFIED CAPTCHA CLAIM", token.claims);
      return token.claims;
    } catch (error) {
      Logger.instance.error(LogSource.API, error);
      throw error;
    }
  }
}
  1. Ensure that your custom AppCheckProvider's getToken() function makes a call to your API (or an https cloud function). Your API function should check if the user has been verified since their last login. If so, use the firebase admin sdk to mint them a new AppCheckToken.

Here's an example of a nextjs api route that uses hCaptcha for verification and mints an AppCheckToken if the user solved the captcha correctly.
(NOTE: other free GDPR compliant bot protection services are available)

import { VercelRequest, VercelResponse } from "@vercel/node";
import FirebaseAdmin from "../../lib/admin";

  async createAppCheckToken(): Promise<{
    token: string;
    expireTimeMillis: number;
  }> {
    const appCheckToken = await FirebaseAdmin.appCheck().createToken(
      process.env.FIREBASE_APP_ID
    );
    if (!appCheckToken) {
      return undefined;
    }
    const expireTimeSeconds = Math.trunc(Date.now() / 1000) + 60 * 60 * 24; // App Check tokens should expire after 1 day
    return {
      token: appCheckToken.token,
      expireTimeMillis: expireTimeSeconds * 1000,
    };
  }

export const verifyAppCheck = async (
  req: VercelRequest,
  res: VercelResponse
): Promise<VercelResponse> => {
  const { body, method } = req;

  // Extract the token and captcha code from the request body
  const { token } = body;

  if (method === "POST") {
    // If token is missing return an error
    if (!token) {
      const message = "please provide token";
      console.error(message);
      return res.status(422).json({
        message,
      });
    }

    let uid: string;

    try {
      const decodedToken = await FirebaseAdmin.auth().verifyIdToken(
        token,
        true
      );
      uid = decodedToken?.uid;

      const userRecord = await FirebaseAdmin.auth().getUser(uid);
      const auth_time =
        new Date(userRecord.metadata.lastSignInTime).getTime() / 1000;
      const customClaims = userRecord?.customClaims;
      const captcha_time = customClaims?.captcha_time;
      const isCaptchaStillValid =
        auth_time && captcha_time && auth_time <= captcha_time;

      if (!isCaptchaStillValid) {
        const message = "captcha no longer valid";
        console.error(message);
        return res.status(403).json({ message });
      }

      // User has already solved a captcha puzzle since their last login on this device.
      // Mint a new app check token
      const appCheckToken = await createAppCheckToken();
      if (!appCheckToken) {
        const message = "app check token generation failed";
        console.error(message);
        return res.status(403).json({ message });
      }

      return res.status(200).json(appCheckToken);
    } catch (error) {
      console.error(error);
      const message = JSON.stringify(error);
      return res.status(403).json({ message });
    }
  }
  const message = `${method} not found`;
  console.error(message);
  // Return 404 if someone pings the API with a method other than POST
  return res.status(404).json({ message });
};

export default verifyAppCheck;
  1. Write a verifyCaptchaClaim() method that you can call on your signup and login pages. This method should perform a check to ensure that the user is not a bot, and if they pass, then update their captcha_time custom claim.
import { VercelRequest, VercelResponse } from "@vercel/node";
import FirebaseAdmin from "../../lib/admin";

  async isCaptchaValid(captcha: string): Promise<boolean> {
    // Ping captcha siteverify API to verify the captcha code you received
    const response = await fetch(`https://hcaptcha.com/siteverify`, {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
      },
      body: `response=${captcha}&secret=${process.env.CAPTCHA_SECRET_KEY}`,
      method: "POST",
    });
    const captchaValidation = await response.json();

    return captchaValidation.success;
  }

 getCurrentTimeInSeconds(): number {
    return Math.trunc(Date.now() / 1000);
  }

  getCaptchaClaims(): { captcha_time: number } {
    const captcha_time = getCurrentTimeInSeconds(); // get time in seconds so it can be easily compared to request.auth.token.auth_time
    const newClaims = {
      captcha_time,
    };
    return newClaims;
  }

export const verifyCaptchaClaim = async (
  req: VercelRequest,
  res: VercelResponse
): Promise<VercelResponse> => {
  const { body, method } = req;

  // Extract the token and captcha code from the request body
  const { token, captcha } = body;

  if (method === "POST") {
    // If token is missing return an error
    if (!token) {
      const message = "please provide token";
      console.error(message);
      return res.status(422).json({
        message,
      });
    }

    let uid: string;

    try {
      const decodedToken = await FirebaseAdmin.auth().verifyIdToken(
        token,
        true
      );
      uid = decodedToken?.uid;

      const userRecord = await FirebaseAdmin.auth().getUser(uid);
      const auth_time =
        new Date(userRecord.metadata.lastSignInTime).getTime() / 1000;
      const customClaims = userRecord?.customClaims;
      const captcha_time = customClaims?.captcha_time;
      const isCaptchaStillValid =
        auth_time && captcha_time && auth_time <= captcha_time;

      if (isCaptchaStillValid) {
        const newClaims = {
          ...(userRecord?.customClaims || {}),
          ...getCaptchaClaims(),
        };
        await FirebaseAdmin.auth().setCustomUserClaims(uid, newClaims);
        return res.status(200).json(newClaims);
      }

      // If captcha is missing return an error
      if (!captcha) {
        const message = "please provide captcha";
        console.error(message);
        return res.status(422).json({
          message,
        });
      }

      const passedChallenge = await isCaptchaValid(
        captcha
      );

      if (!passedChallenge) {
        const message = "captcha challenge failed";
        console.error(message);
        return res.status(403).json({
          message,
        });
      }

      const newClaims = {
        ...(userRecord?.customClaims || {}),
        ...getCaptchaClaims(),
      };
      await FirebaseAdmin.auth().setCustomUserClaims(uid, newClaims);

      return res.status(200).json(newClaims);
    } catch (error) {
      console.error(error);
      const message = JSON.stringify(error);
      return res.status(403).json({ message });
    }
  }
  const message = `${method} not found`;
  console.error(message);
  // Return 404 if someone pings the API with a method other than POST
  return res.status(404).json({ message });
};

export default verifyCaptchaClaim;
  1. In your app's signup and login pages, include a GDPR compliant service that the user can use to verify that they are not a bot (e.g. an hCaptcha component), and pass the user's verification answer to your AppCheckVerifier.instance.verifyCaptchaClaim() method.

If you use a service like hCaptcha in "invisible" mode, remember that you must display a legal notice that informs the user about the service and any of it's terms or privacy policies in order to be GDPR compliant.

import Captcha from "@hcaptcha/react-hcaptcha";
import FirebaseClient from "../../lib/client";
import { AppCheckVerifier } from "../../utils";

const Login = (): JSX.Element | null => {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  const captchaActionsRef = useRef<CaptchaActions>();

  const handleVerifiedSubmit =
    async (captcha: string) => {
      await FirebaseClient.auth().signInWithEmailAndPassword(email, password);
      await AppCheckVerifier.instance.verifyCaptchaClaim(captcha);
    };

  const handleCaptchaChange =
    async (captcha) => {
      if (captcha) {
        handleVerifiedSubmit(captcha);
      }
    };

  const handleSubmit =
    async (e: React.FormEvent | React.MouseEvent) => {
      e.preventDefault();
      if (captchaActionsRef.current) {
        captchaActionsRef.current.execute();
      }
    };

  return (
    <form onSubmit={handleSubmit}>
      <input
        onChange={(e): void => setEmail(e.target.value)}
        required
        type="email"
        name="email"
        placeholder="Email"
      />
      <input
        onChange={(e): void => setPassword(e.target.value)}
        required
        type="password"
        name="password"
        placeholder="Password"
      />
      <Captcha
        size="invisible"
        ref={captchaActionsRef}
        sitekey={process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY}
        onVerify={handleCaptchaChange}
      />
      <button type="submit">Login</button>
      <Markdown>
        Protected by hCaptcha. Its [Terms](https://hcaptcha.com/terms) and [Privacy Policy](https://hcaptcha.com/privacy) apply.
      </Markdown>
    </form>
  );
};
  1. Finally, make sure you call AppCheckVerifier.instance.verify() before you use any service that requires AppCheck.

For example:

 AppCheckVerifier.instance.verify();
 FirebaseClient.storage().ref().child(path).put(file);
AppCheckVerifier.instance.verify();
const myCloudFunction = FirebaseClient.functions().httpsCallable("myCloudFunction");
await myCloudFunction();

And that's it!

As you can see, there's quite a bit of setup you'll need to do to make AppCheck GDPR compliant. But it is possible, so as long as you are aware of the privacy issues present in the default AppCheck setup and are aware of the strategies you can use to work around them.

I'm not sure if it's on firebase's roadmap anytime soon, but I think it would help developers out if there was better documentation on how a user can setup custom AppCheck providers. The current documentation doesn't really explain specifics around when the activate() method should be called and doesn't explain that you may need to delay activation until after consent is received from the user.

https://firebase.google.com/docs/app-check/web
https://firebase.google.com/docs/app-check/web-custom-provider

Also, though the documentation notes that you may use a custom attestation provider, the firebase console's AppCheck interface seems to imply that the use of reCAPTCHA is required for AppCheck. To enable AppCheck you must click the blue plus button next to the label "reCAPTCHA". And when you click that button, it says

By registering your Web app for App Check your app will use reCAPTCHA v3.

Which is not necessarily true. If you provide your own custom attestation provider in your codebase, then you can still use AppCheck without reCAPTCHA. Even more worrying, is if you do click the button to register your app for AppCheck, there doesn't seem to be a way to disable reCAPTCHA through the firebase console interface. Or even an option to unregister your app from AppCheck altogether. (Again, reCAPTCHA won't actually be used unless you call appCheck.activate() without providing your own custom attestation provider. But the interface of the console is a bit misleading because it seems to imply that reCAPTCHA is your only option if you are using AppCheck for a web app.)

I understand that, business-wise, it is not really feasible for firebase to recommend alternatives to reCAPTCHA, but even just providing some basic information about the privacy, cookies, and consent issues present in AppCheck's current default web implementation would help out developers who may run into legal trouble if they start using AppCheck on their website without being aware of the minutia of its current implementation.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4