Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support consumeAppCheckToken option for callable functions #1374

Merged
merged 13 commits into from
May 5, 2023
44 changes: 38 additions & 6 deletions src/common/providers/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
taeold marked this conversation as resolved.
Show resolved Hide resolved
*/
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.
taeold marked this conversation as resolved.
Show resolved Hide resolved
*
* 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;
}

/**
Expand Down Expand Up @@ -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<CallableTokenStatus> {
async function checkTokens(
req: Request,
ctx: CallableContext,
options: CallableOptions
): Promise<CallableTokenStatus> {
const verifications: CallableTokenStatus = {
app: "INVALID",
auth: "INVALID",
Expand All @@ -546,7 +566,7 @@ async function checkTokens(req: Request, ctx: CallableContext): Promise<Callable
verifications.auth = await checkAuthToken(req, ctx);
}),
Promise.resolve().then(async () => {
verifications.app = await checkAppCheckToken(req, ctx);
verifications.app = await checkAppCheckToken(req, ctx, options);
}),
]);

Expand Down Expand Up @@ -607,18 +627,29 @@ export async function checkAuthToken(
}

/** @internal */
async function checkAppCheckToken(req: Request, ctx: CallableContext): Promise<TokenStatus> {
async function checkAppCheckToken(
req: Request,
ctx: CallableContext,
options: CallableOptions
): Promise<TokenStatus> {
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";
Expand All @@ -635,6 +666,7 @@ type v2CallableHandler<Req, Res> = (request: CallableRequest<Req>) => Res;
export interface CallableOptions {
cors: cors.CorsOptions;
enforceAppCheck?: boolean;
consumeAppCheckToken?: boolean;
}

/** @internal */
Expand Down Expand Up @@ -692,7 +724,7 @@ function wrapOnCallHandler<Req = any, Res = any>(
}
}

const tokenStatus = await checkTokens(req, context);
const tokenStatus = await checkTokens(req, context, options);
if (tokenStatus.auth === "INVALID") {
throw new HttpsError("unauthenticated", "Unauthenticated");
}
Expand Down
19 changes: 18 additions & 1 deletion src/v2/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
taeold marked this conversation as resolved.
Show resolved Hide resolved
* Tokens that are found to be already consumed will return app data as alreadyConsumed.
taeold marked this conversation as resolved.
Show resolved Hide resolved
taeold marked this conversation as resolved.
Show resolved Hide resolved
*
* Tokens are only considered to be consumed by calling the VerifyAppCheckToken method and setting this value to true;
taeold marked this conversation as resolved.
Show resolved Hide resolved
* 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.
*
Expand Down
17 changes: 17 additions & 0 deletions src/v2/providers/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
taeold marked this conversation as resolved.
Show resolved Hide resolved
*
* 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;
}

/**
Expand Down