@@ -28,7 +28,7 @@ import * as _ from 'lodash';
2828import { apps } from '../apps' ;
2929import { HttpsFunction , optionsToTrigger , Runnable } from '../cloud-functions' ;
3030import { DeploymentOptions } from '../function-configuration' ;
31- import { warn , error } from '../logger' ;
31+ import { error , info , warn } from '../logger' ;
3232
3333/** @hidden */
3434export interface Request extends express . Request {
@@ -248,6 +248,14 @@ export class HttpsError extends Error {
248248 * The interface for metadata for the API as passed to the handler.
249249 */
250250export interface CallableContext {
251+ /**
252+ * The result of decoding and verifying a Firebase AppCheck token.
253+ */
254+ app ?: {
255+ appId : string ;
256+ token : firebase . appCheck . DecodedAppCheckToken ;
257+ } ;
258+
251259 /**
252260 * The result of decoding and verifying a Firebase Auth ID token.
253261 */
@@ -411,6 +419,108 @@ export function decode(data: any): any {
411419 return data ;
412420}
413421
422+ /**
423+ * Be careful when changing token status values.
424+ *
425+ * Users are encouraged to setup log-based metric based on these values, and
426+ * changing their values may cause their metrics to break.
427+ *
428+ */
429+ /** @hidden */
430+ type TokenStatus = 'MISSING' | 'VALID' | 'INVALID' ;
431+
432+ /** @hidden */
433+ interface CallableTokenStatus {
434+ app : TokenStatus ;
435+ auth : TokenStatus ;
436+ }
437+
438+ /**
439+ * Check and verify tokens included in the requests. Once verified, tokens
440+ * are injected into the callable context.
441+ *
442+ * @param {Request } req - Request sent to the Callable function.
443+ * @param {CallableContext } ctx - Context to be sent to callable function handler.
444+ * @return {CallableTokenStatus } Status of the token verifications.
445+ */
446+ /** @hidden */
447+ async function checkTokens (
448+ req : Request ,
449+ ctx : CallableContext
450+ ) : Promise < CallableTokenStatus > {
451+ const verifications : CallableTokenStatus = {
452+ app : 'MISSING' ,
453+ auth : 'MISSING' ,
454+ } ;
455+
456+ const appCheck = req . header ( 'X-Firebase-AppCheck' ) ;
457+ if ( appCheck ) {
458+ verifications . app = 'INVALID' ;
459+ try {
460+ if ( ! apps ( ) . admin . appCheck ) {
461+ throw new Error (
462+ 'Cannot validate AppCheck token. Please uupdate Firebase Admin SDK to >= v9.8.0'
463+ ) ;
464+ }
465+ const appCheckToken = await apps ( )
466+ . admin . appCheck ( )
467+ . verifyToken ( appCheck ) ;
468+ ctx . app = {
469+ appId : appCheckToken . appId ,
470+ token : appCheckToken . token ,
471+ } ;
472+ verifications . app = 'VALID' ;
473+ } catch ( err ) {
474+ warn ( 'Failed to validate AppCheck token.' , err ) ;
475+ }
476+ }
477+
478+ const authorization = req . header ( 'Authorization' ) ;
479+ if ( authorization ) {
480+ verifications . auth = 'INVALID' ;
481+ const match = authorization . match ( / ^ B e a r e r ( .* ) $ / ) ;
482+ if ( match ) {
483+ const idToken = match [ 1 ] ;
484+ try {
485+ const authToken = await apps ( )
486+ . admin . auth ( )
487+ . verifyIdToken ( idToken ) ;
488+
489+ verifications . auth = 'VALID' ;
490+ ctx . auth = {
491+ uid : authToken . uid ,
492+ token : authToken ,
493+ } ;
494+ } catch ( err ) {
495+ warn ( 'Failed to validate auth token.' , err ) ;
496+ }
497+ }
498+ }
499+
500+ const logPayload = {
501+ verifications,
502+ 'logging.googleapis.com/labels' : {
503+ 'firebase-log-type' : 'callable-request-verification' ,
504+ } ,
505+ } ;
506+
507+ const errs = [ ] ;
508+ if ( verifications . app === 'INVALID' ) {
509+ errs . push ( 'AppCheck token was rejected.' ) ;
510+ }
511+ if ( verifications . auth === 'INVALID' ) {
512+ errs . push ( 'Auth token was rejected.' ) ;
513+ }
514+
515+ if ( errs . length == 0 ) {
516+ info ( 'Callable request verification passed' , logPayload ) ;
517+ } else {
518+ warn ( `Callable request verification failed: ${ errs . join ( ' ' ) } ` , logPayload ) ;
519+ }
520+
521+ return verifications ;
522+ }
523+
414524/** @hidden */
415525const corsHandler = cors ( { origin : true , methods : 'POST' } ) ;
416526
@@ -427,25 +537,9 @@ export function _onCallWithOptions(
427537 }
428538
429539 const context : CallableContext = { rawRequest : req } ;
430-
431- const authorization = req . header ( 'Authorization' ) ;
432- if ( authorization ) {
433- const match = authorization . match ( / ^ B e a r e r ( .* ) $ / ) ;
434- if ( ! match ) {
435- throw new HttpsError ( 'unauthenticated' , 'Unauthenticated' ) ;
436- }
437- const idToken = match [ 1 ] ;
438- try {
439- const authToken = await apps ( )
440- . admin . auth ( )
441- . verifyIdToken ( idToken ) ;
442- context . auth = {
443- uid : authToken . uid ,
444- token : authToken ,
445- } ;
446- } catch ( err ) {
447- throw new HttpsError ( 'unauthenticated' , 'Unauthenticated' ) ;
448- }
540+ const tokenStatus = await checkTokens ( req , context ) ;
541+ if ( tokenStatus . app === 'INVALID' || tokenStatus . auth === 'INVALID' ) {
542+ throw new HttpsError ( 'unauthenticated' , 'Unauthenticated' ) ;
449543 }
450544
451545 const instanceId = req . header ( 'Firebase-Instance-ID-Token' ) ;
0 commit comments