Skip to content

Commit 0b47d40

Browse files
authored
Move to enforceAppCheck option (#1145)
1 parent ea5edf9 commit 0b47d40

File tree

7 files changed

+124
-17
lines changed

7 files changed

+124
-17
lines changed

spec/common/providers/https.spec.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,10 @@ describe('onCallHandler', () => {
370370
const appCheckToken = generateUnsignedAppCheckToken(projectId, appId);
371371
await runCallableTest({
372372
httpRequest: mockRequest(null, 'application/json', { appCheckToken }),
373+
callableOption: {
374+
cors: { origin: true, methods: 'POST' },
375+
enforceAppCheck: true,
376+
},
373377
expectedData: null,
374378
callableFunction: (data, context) => {
375379
return;
@@ -390,7 +394,7 @@ describe('onCallHandler', () => {
390394
});
391395
});
392396

393-
it('should handle bad AppCheck token with callable option', async () => {
397+
it('should handle bad AppCheck token with enforcement disabled', async () => {
394398
await runCallableTest({
395399
httpRequest: mockRequest(null, 'application/json', {
396400
appCheckToken: 'FAKE',
@@ -404,7 +408,7 @@ describe('onCallHandler', () => {
404408
},
405409
callableOption: {
406410
cors: { origin: true, methods: 'POST' },
407-
allowInvalidAppCheckToken: true,
411+
enforceAppCheck: false,
408412
},
409413
expectedHttpResponse: {
410414
status: 200,
@@ -414,6 +418,64 @@ describe('onCallHandler', () => {
414418
});
415419
});
416420

421+
it('should handle bad AppCheck token with enforcement enabled', async () => {
422+
await runCallableTest({
423+
httpRequest: mockRequest(null, 'application/json', {
424+
appCheckToken: 'FAKE',
425+
}),
426+
expectedData: null,
427+
callableFunction: (data, context) => {
428+
return;
429+
},
430+
callableFunction2: (request) => {
431+
return;
432+
},
433+
callableOption: {
434+
cors: { origin: true, methods: 'POST' },
435+
enforceAppCheck: true,
436+
},
437+
expectedHttpResponse: {
438+
status: 401,
439+
headers: expectedResponseHeaders,
440+
body: {
441+
error: {
442+
message: 'Unauthenticated',
443+
status: 'UNAUTHENTICATED',
444+
},
445+
},
446+
},
447+
});
448+
});
449+
450+
it('should handle no AppCheck token with enforcement enabled', async () => {
451+
await runCallableTest({
452+
httpRequest: mockRequest(null, 'application/json', {
453+
appCheckToken: 'MISSING',
454+
}),
455+
expectedData: null,
456+
callableFunction: (data, context) => {
457+
return;
458+
},
459+
callableFunction2: (request) => {
460+
return;
461+
},
462+
callableOption: {
463+
cors: { origin: true, methods: 'POST' },
464+
enforceAppCheck: true,
465+
},
466+
expectedHttpResponse: {
467+
status: 401,
468+
headers: expectedResponseHeaders,
469+
body: {
470+
error: {
471+
message: 'Unauthenticated',
472+
status: 'UNAUTHENTICATED',
473+
},
474+
},
475+
},
476+
});
477+
});
478+
417479
it('should handle instance id', async () => {
418480
await runCallableTest({
419481
httpRequest: mockRequest(null, 'application/json', {

src/common/providers/https.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ type v2CallableHandler<Req, Res> = (request: CallableRequest<Req>) => Res;
643643
/** @internal **/
644644
export interface CallableOptions {
645645
cors: cors.CorsOptions;
646-
allowInvalidAppCheckToken?: boolean;
646+
enforceAppCheck?: boolean;
647647
}
648648

649649
/** @internal */
@@ -679,7 +679,16 @@ function wrapOnCallHandler<Req = any, Res = any>(
679679
if (tokenStatus.auth === 'INVALID') {
680680
throw new HttpsError('unauthenticated', 'Unauthenticated');
681681
}
682-
if (tokenStatus.app === 'INVALID' && !options.allowInvalidAppCheckToken) {
682+
if (tokenStatus.app === 'INVALID') {
683+
if (options.enforceAppCheck) {
684+
throw new HttpsError('unauthenticated', 'Unauthenticated');
685+
} else {
686+
logger.warn(
687+
'Allowing request with invalid AppCheck token because enforcement is disabled'
688+
);
689+
}
690+
}
691+
if (tokenStatus.app === 'MISSING' && options.enforceAppCheck) {
683692
throw new HttpsError('unauthenticated', 'Unauthenticated');
684693
}
685694

src/function-builder.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,13 @@ function assertRuntimeOptionsValid(runtimeOptions: RuntimeOptions): boolean {
242242
}
243243
}
244244

245+
if ('allowInvalidAppCheckToken' in runtimeOptions) {
246+
throw new Error(
247+
'runWith option "allowInvalidAppCheckToken" has been inverted and ' +
248+
'renamed "enforceAppCheck"'
249+
);
250+
}
251+
245252
return true;
246253
}
247254

src/function-configuration.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,18 @@ export interface RuntimeOptions {
161161
*/
162162
invoker?: 'public' | 'private' | string | string[];
163163

164-
/*
165-
* Allow requests with invalid App Check tokens on callable functions.
166-
*/
167-
allowInvalidAppCheckToken?: boolean;
168-
169164
/*
170165
* Secrets to bind to a function instance.
171166
*/
172167
secrets?: string[];
168+
169+
/**
170+
* Determines whether Firebase AppCheck is enforced.
171+
* When true, requests with invalid tokens autorespond with a 401
172+
* (Unauthorized) error.
173+
* When false, requests with invalid tokens set context.app to undefiend.
174+
*/
175+
enforceAppCheck?: boolean;
173176
}
174177

175178
export interface DeploymentOptions extends RuntimeOptions {

src/providers/https.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function _onCallWithOptions(
110110
handler(data, context);
111111
const func: any = onCallHandler(
112112
{
113-
allowInvalidAppCheckToken: options.allowInvalidAppCheckToken,
113+
enforceAppCheck: options.enforceAppCheck,
114114
cors: { origin: true, methods: 'POST' },
115115
},
116116
fixedLen

src/v2/options.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ export interface GlobalOptions {
189189
* Secrets to bind to a function.
190190
*/
191191
secrets?: string[];
192+
193+
/**
194+
* Determines whether Firebase AppCheck is enforced.
195+
* When true, requests with invalid tokens autorespond with a 401
196+
* (Unauthorized) error.
197+
* When false, requests with invalid tokens set event.app to undefiend.
198+
*/
199+
enforceAppCheck?: boolean;
192200
}
193201

194202
let globalOptions: GlobalOptions | undefined;
@@ -216,7 +224,8 @@ export function getGlobalOptions(): GlobalOptions {
216224
/**
217225
* Additional fields that can be set on any event-handling Cloud Function.
218226
*/
219-
export interface EventHandlerOptions extends GlobalOptions {
227+
export interface EventHandlerOptions
228+
extends Omit<GlobalOptions, 'enforceAppCheck'> {
220229
/** Whether failed executions should be delivered again. */
221230
retry?: boolean;
222231
}

src/v2/providers/https.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import { GlobalOptions, SupportedRegion } from '../options';
4444
export { Request, CallableRequest, FunctionsErrorCode, HttpsError };
4545

4646
/**
47-
* Options that can be set on an individual HTTPS function.
47+
* Options that can be set on an onRequest HTTPS function.
4848
*/
4949
export interface HttpsOptions extends Omit<GlobalOptions, 'region'> {
5050
/** HTTP functions can override global options and can specify multiple regions to deploy to. */
@@ -150,6 +150,19 @@ export interface HttpsOptions extends Omit<GlobalOptions, 'region'> {
150150
retry?: boolean;
151151
}
152152

153+
/**
154+
* Options that can be set on a callable HTTPS function.
155+
*/
156+
export interface CallableOptions extends HttpsOptions {
157+
/**
158+
* Determines whether Firebase AppCheck is enforced.
159+
* When true, requests with invalid tokens autorespond with a 401
160+
* (Unauthorized) error.
161+
* When false, requests with invalid tokens set event.app to undefiend.
162+
*/
163+
enforceAppCheck?: boolean;
164+
}
165+
153166
/**
154167
* Handles HTTPS requests.
155168
*/
@@ -298,7 +311,7 @@ export function onRequest(
298311
* @returns A function that you can export and deploy.
299312
*/
300313
export function onCall<T = any, Return = any | Promise<any>>(
301-
opts: HttpsOptions,
314+
opts: CallableOptions,
302315
handler: (request: CallableRequest<T>) => Return
303316
): CallableFunction<T, Return>;
304317
/**
@@ -310,15 +323,15 @@ export function onCall<T = any, Return = any | Promise<any>>(
310323
handler: (request: CallableRequest<T>) => Return
311324
): CallableFunction<T, Return>;
312325
export function onCall<T = any, Return = any | Promise<any>>(
313-
optsOrHandler: HttpsOptions | ((request: CallableRequest<T>) => Return),
326+
optsOrHandler: CallableOptions | ((request: CallableRequest<T>) => Return),
314327
handler?: (request: CallableRequest<T>) => Return
315328
): CallableFunction<T, Return> {
316-
let opts: HttpsOptions;
329+
let opts: CallableOptions;
317330
if (arguments.length == 1) {
318331
opts = {};
319332
handler = optsOrHandler as (request: CallableRequest<T>) => Return;
320333
} else {
321-
opts = optsOrHandler as HttpsOptions;
334+
opts = optsOrHandler as CallableOptions;
322335
}
323336

324337
const origin = isDebugFeatureEnabled('enableCors')
@@ -331,7 +344,11 @@ export function onCall<T = any, Return = any | Promise<any>>(
331344
// fix the length to prevent api versions from being mismatched.
332345
const fixedLen = (req: CallableRequest<T>) => handler(req);
333346
const func: any = onCallHandler(
334-
{ cors: { origin, methods: 'POST' } },
347+
{
348+
cors: { origin, methods: 'POST' },
349+
enforceAppCheck:
350+
opts.enforceAppCheck ?? options.getGlobalOptions().enforceAppCheck,
351+
},
335352
fixedLen
336353
);
337354

0 commit comments

Comments
 (0)