Skip to content

Commit 63bd14d

Browse files
authored
Add an option to disable rejection of requests with invalid App Check token for callable functions. (#989)
Since releasing App Check integration for Callable Functions, we've received several requests from our users to make it possible turn App Check enforcement off. By default, if a request includes an App Check token, callable functions will verify the token, and - if the token is invalid - reject the request. This makes it hard for developers to onboard to App Check, especially for developers that want to "soft launch" App Check integration to measure the App Check enforcement would have on its users. The change here adds a `runWith` option to allow requests with invalid App check token to continue to user code execution, e.g. ```js exports.yourCallableFunction = functions. .runWith({ allowInvalidAppCheckToken: true // Opt-out: Invalid App Check token cont. to user code. }). .https.onCall( (data, context) => { // Requests with an invalid App Check token are not rejected. // // context.app will be undefined if the request: // 1) Does not include an App Check token // 2) Includes an invalid App Check token if (context.app == undefined) { // Users can manually inspect raw request header to check whether an App Check // token was provided in the request. const rawToken = context.rawRequest.header['X-Firebase-AppCheck']; if (rawToken == undefined) { throw new functions.https.HttpsError( 'failed-precondition', 'The function must be called from an App Check verified app.' ); } else { throw new functions.https.HttpsError( 'unauthenticated', 'Provided App Check token failed to validate.' ); } }, } ); ```
1 parent 0479817 commit 63bd14d

File tree

6 files changed

+61
-7
lines changed

6 files changed

+61
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
- GCS Enhancement
2+
- Add option to allow callable functions to ignore invalid App Check tokens.

spec/common/providers/https.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ interface CallTest {
4040

4141
callableFunction2: (request: https.CallableRequest<any>) => any;
4242

43+
callableOption?: https.CallableOptions;
44+
4345
// The expected shape of the http response returned to the callable SDK.
4446
expectedHttpResponse: RunHandlerResult;
4547
}
@@ -104,7 +106,10 @@ function runHandler(
104106

105107
// Runs a CallTest test.
106108
async function runTest(test: CallTest): Promise<any> {
107-
const opts = { origin: true, methods: 'POST' };
109+
const opts = {
110+
cors: { origin: true, methods: 'POST' },
111+
...test.callableOption,
112+
};
108113
const callableFunctionV1 = https.onCallHandler(opts, (data, context) => {
109114
expect(data).to.deep.equal(test.expectedData);
110115
return test.callableFunction(data, context);
@@ -470,6 +475,30 @@ describe('onCallHandler', () => {
470475
});
471476
});
472477

478+
it('should handle bad AppCheck token with callable option', async () => {
479+
await runTest({
480+
httpRequest: mockRequest(null, 'application/json', {
481+
appCheckToken: 'FAKE',
482+
}),
483+
expectedData: null,
484+
callableFunction: (data, context) => {
485+
return;
486+
},
487+
callableFunction2: (request) => {
488+
return;
489+
},
490+
callableOption: {
491+
cors: { origin: true, methods: 'POST' },
492+
allowInvalidAppCheckToken: true,
493+
},
494+
expectedHttpResponse: {
495+
status: 200,
496+
headers: expectedResponseHeaders,
497+
body: { result: null },
498+
},
499+
});
500+
});
501+
473502
it('should handle instance id', async () => {
474503
await runTest({
475504
httpRequest: mockRequest(null, 'application/json', {

src/common/providers/https.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -592,16 +592,22 @@ async function checkTokens(
592592
type v1Handler = (data: any, context: CallableContext) => any | Promise<any>;
593593
type v2Handler<Req, Res> = (request: CallableRequest<Req>) => Res;
594594

595+
/** @hidden **/
596+
export interface CallableOptions {
597+
cors: cors.CorsOptions;
598+
allowInvalidAppCheckToken?: boolean;
599+
}
600+
595601
/** @hidden */
596602
export function onCallHandler<Req = any, Res = any>(
597-
options: cors.CorsOptions,
603+
options: CallableOptions,
598604
handler: v1Handler | v2Handler<Req, Res>
599605
): (req: Request, res: express.Response) => Promise<void> {
600-
const wrapped = wrapOnCallHandler(handler);
606+
const wrapped = wrapOnCallHandler(options, handler);
601607
return (req: Request, res: express.Response) => {
602608
return new Promise((resolve) => {
603609
res.on('finish', resolve);
604-
cors(options)(req, res, () => {
610+
cors(options.cors)(req, res, () => {
605611
resolve(wrapped(req, res));
606612
});
607613
});
@@ -610,6 +616,7 @@ export function onCallHandler<Req = any, Res = any>(
610616

611617
/** @internal */
612618
function wrapOnCallHandler<Req = any, Res = any>(
619+
options: CallableOptions,
613620
handler: v1Handler | v2Handler<Req, Res>
614621
): (req: Request, res: express.Response) => Promise<void> {
615622
return async (req: Request, res: express.Response): Promise<void> => {
@@ -621,7 +628,10 @@ function wrapOnCallHandler<Req = any, Res = any>(
621628

622629
const context: CallableContext = { rawRequest: req };
623630
const tokenStatus = await checkTokens(req, context);
624-
if (tokenStatus.app === 'INVALID' || tokenStatus.auth === 'INVALID') {
631+
if (tokenStatus.auth === 'INVALID') {
632+
throw new HttpsError('unauthenticated', 'Unauthenticated');
633+
}
634+
if (tokenStatus.app === 'INVALID' && !options.allowInvalidAppCheckToken) {
625635
throw new HttpsError('unauthenticated', 'Unauthenticated');
626636
}
627637

src/function-configuration.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ export interface RuntimeOptions {
160160
* Invoker to set access control on https functions.
161161
*/
162162
invoker?: 'public' | 'private' | string | string[];
163+
164+
/*
165+
* Allow requests with invalid App Check tokens on callable functions.
166+
*/
167+
allowInvalidAppCheckToken?: boolean;
163168
}
164169

165170
export interface DeploymentOptions extends RuntimeOptions {

src/providers/https.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,13 @@ export function _onCallWithOptions(
9090
// in another handler to avoid accidentally triggering the v2 API
9191
const fixedLen = (data: any, context: CallableContext) =>
9292
handler(data, context);
93-
const func: any = onCallHandler({ origin: true, methods: 'POST' }, fixedLen);
93+
const func: any = onCallHandler(
94+
{
95+
allowInvalidAppCheckToken: options.allowInvalidAppCheckToken,
96+
cors: { origin: true, methods: 'POST' },
97+
},
98+
fixedLen
99+
);
94100

95101
func.__trigger = {
96102
labels: {},

src/v2/providers/https.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ export function onCall<T = any, Return = any | Promise<any>>(
157157
// onCallHandler sniffs the function length to determine which API to present.
158158
// fix the length to prevent api versions from being mismatched.
159159
const fixedLen = (req: CallableRequest<T>) => handler(req);
160-
const func: any = onCallHandler({ origin, methods: 'POST' }, fixedLen);
160+
const func: any = onCallHandler(
161+
{ cors: { origin, methods: 'POST' } },
162+
fixedLen
163+
);
161164

162165
Object.defineProperty(func, '__trigger', {
163166
get: () => {

0 commit comments

Comments
 (0)