From f9e330d1c1c490bfe39c5acb42e767e4f24a6ae4 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 12 Apr 2023 17:32:49 -0700 Subject: [PATCH] Enhance App Check support. --- src/common/providers/https.ts | 44 ++++++++++++++++++++++++++++++----- src/v2/options.ts | 19 ++++++++++++++- src/v2/providers/https.ts | 17 ++++++++++++++ 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/common/providers/https.ts b/src/common/providers/https.ts index 9961790f7..32161248f 100644 --- a/src/common/providers/https.ts +++ b/src/common/providers/https.ts @@ -51,8 +51,24 @@ export interface Request extends express.Request { * The interface for AppCheck tokens verified in Callable functions */ export interface AppCheckData { + /** + * The App ID that App Check token belonged to. + */ appId: string; + /** + * Decoded App Check token. + */ token: DecodedAppCheckToken; + /** + * Indicates if the token has been consumed. + * + * @remarks + * If `false`, App Check has not validated the token, and will be marked as consumed for future use. + * + * If `true`, the caller is trying to reuse a consumed token. Consider taking precautions, such as rejecting the + * request or requiring additional security checks. + */ + alreadyConsumed?: boolean; } /** @@ -535,7 +551,11 @@ export function unsafeDecodeAppCheckToken(token: string): DecodedAppCheckToken { * @returns {CallableTokenStatus} Status of the token verifications. */ /** @internal */ -async function checkTokens(req: Request, ctx: CallableContext): Promise { +async function checkTokens( + req: Request, + ctx: CallableContext, + options: CallableOptions +): Promise { const verifications: CallableTokenStatus = { app: "INVALID", auth: "INVALID", @@ -546,7 +566,7 @@ async function checkTokens(req: Request, ctx: CallableContext): Promise { - verifications.app = await checkAppCheckToken(req, ctx); + verifications.app = await checkAppCheckToken(req, ctx, options); }), ]); @@ -607,18 +627,29 @@ export async function checkAuthToken( } /** @internal */ -async function checkAppCheckToken(req: Request, ctx: CallableContext): Promise { +async function checkAppCheckToken( + req: Request, + ctx: CallableContext, + options: CallableOptions +): Promise { const appCheck = req.header("X-Firebase-AppCheck"); if (!appCheck) { return "MISSING"; } try { - let appCheckData; + let appCheckData: AppCheckData; if (isDebugFeatureEnabled("skipTokenVerification")) { const decodedToken = unsafeDecodeAppCheckToken(appCheck); appCheckData = { appId: decodedToken.app_id, token: decodedToken }; + if (options.consumeAppCheckToken) { + appCheckData.alreadyConsumed = false; + } } else { - appCheckData = await getAppCheck(getApp()).verifyToken(appCheck); + if (options.consumeAppCheckToken) { + appCheckData = await getAppCheck(getApp()).verifyToken(appCheck, { consume: true }); + } else { + appCheckData = await getAppCheck(getApp()).verifyToken(appCheck); + } } ctx.app = appCheckData; return "VALID"; @@ -635,6 +666,7 @@ type v2CallableHandler = (request: CallableRequest) => Res; export interface CallableOptions { cors: cors.CorsOptions; enforceAppCheck?: boolean; + consumeAppCheckToken?: boolean; } /** @internal */ @@ -692,7 +724,7 @@ function wrapOnCallHandler( } } - const tokenStatus = await checkTokens(req, context); + const tokenStatus = await checkTokens(req, context, options); if (tokenStatus.auth === "INVALID") { throw new HttpsError("unauthenticated", "Unauthenticated"); } diff --git a/src/v2/options.ts b/src/v2/options.ts index f489588e1..1725bad72 100644 --- a/src/v2/options.ts +++ b/src/v2/options.ts @@ -200,10 +200,27 @@ export interface GlobalOptions { * @remarks * When true, requests with invalid tokens autorespond with a 401 * (Unauthorized) error. - * When false, requests with invalid tokens set event.app to undefiend. + * When false, requests with invalid tokens set event.app to undefined. */ enforceAppCheck?: boolean; + /** + * Determines whether Firebase App Check token is consumed on request. Defaults to false. + * + * @remarks + * Set this to true to enable the App Check replay protection feature by consuming App Check token on callable request. + * Tokens that are found to be already consumed will return app data as alreadyConsumed. + * + * Tokens are only considered to be consumed by calling the VerifyAppCheckToken method and setting this value to true; + * other uses of the token do not consume it. + * + * This replay protection feature requires an additional network call to the App Check backend and forces the clients + * to obtain a fresh attestation from the chosen attestation providers. This can therefore negatively impact + * performance and can potentially deplete your attestation providers' quotas faster. Use this feature only for + * protecting low volume, security critical, or expensive operations. + */ + consumeAppCheckToken?: boolean; + /** * Controls whether function configuration modified outside of function source is preserved. Defaults to false. * diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index 80def4273..3daa866de 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -167,6 +167,23 @@ export interface CallableOptions extends HttpsOptions { * When false, requests with invalid tokens set event.app to undefiend. */ enforceAppCheck?: boolean; + + /** + * Determines whether Firebase App Check token is consumed on request. Defaults to false. + * + * @remarks + * Set this to true to enable the App Check replay protection feature by consuming App Check token on callable request. + * Tokens that are found to be already consumed will return app data as alreadyConsumed. + * + * Tokens are only considered to be consumed by calling the VerifyAppCheckToken method and setting this value to true; + * other uses of the token do not consume it. + * + * This replay protection feature requires an additional network call to the App Check backend and forces the clients + * to obtain a fresh attestation from the chosen attestation providers. This can therefore negatively impact + * performance and can potentially deplete your attestation providers' quotas faster. Use this feature only for + * protecting low volume, security critical, or expensive operations. + */ + consumeAppCheckToken?: boolean; } /**