Skip to content

Commit

Permalink
Enhance App Check support.
Browse files Browse the repository at this point in the history
  • Loading branch information
taeold committed Apr 13, 2023
1 parent 74a5553 commit f9e330d
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 7 deletions.
44 changes: 38 additions & 6 deletions src/common/providers/https.ts
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.
*/
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;
}

/**
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
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.
* 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.
*
Expand Down
17 changes: 17 additions & 0 deletions src/v2/providers/https.ts
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.
*
* 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

0 comments on commit f9e330d

Please sign in to comment.