11import { expect } from 'chai' ;
22import * as express from 'express' ;
33import * as firebase from 'firebase-admin' ;
4+ import * as sinon from 'sinon' ;
45
56import { apps as appsNamespace } from '../../../src/apps' ;
67import * as https from '../../../src/common/providers/https' ;
8+ import * as debug from '../../../src/common/debug' ;
79import * as mocks from '../../fixtures/credential/key.json' ;
810import {
911 expectedResponseHeaders ,
1012 generateAppCheckToken ,
1113 generateIdToken ,
14+ generateUnsignedAppCheckToken ,
15+ generateUnsignedIdToken ,
1216 mockFetchAppCheckPublicJwks ,
1317 mockFetchPublicKeys ,
1418 mockRequest ,
1519} from '../../fixtures/mockrequest' ;
20+ import {
21+ CallableContext ,
22+ CallableRequest ,
23+ unsafeDecodeAppCheckToken ,
24+ unsafeDecodeIdToken ,
25+ } from '../../../src/common/providers/https' ;
1626
1727/**
1828 * RunHandlerResult contains the data from an express.Response.
@@ -133,6 +143,64 @@ async function runTest(test: CallTest): Promise<any> {
133143 expect ( responseV2 . status ) . to . equal ( test . expectedHttpResponse . status ) ;
134144}
135145
146+ function checkAuthContext (
147+ context : CallableContext ,
148+ projectId : string ,
149+ userId : string
150+ ) {
151+ expect ( context . auth ) . to . not . be . undefined ;
152+ expect ( context . auth ) . to . not . be . null ;
153+ expect ( context . auth . uid ) . to . equal ( userId ) ;
154+ expect ( context . auth . token . uid ) . to . equal ( userId ) ;
155+ expect ( context . auth . token . sub ) . to . equal ( userId ) ;
156+ expect ( context . auth . token . aud ) . to . equal ( projectId ) ;
157+ expect ( context . instanceIdToken ) . to . be . undefined ;
158+ }
159+
160+ function checkAppCheckContext (
161+ context : CallableContext ,
162+ projectId : string ,
163+ appId : string
164+ ) {
165+ expect ( context . app ) . to . not . be . undefined ;
166+ expect ( context . app ) . to . not . be . null ;
167+ expect ( context . app . appId ) . to . equal ( appId ) ;
168+ expect ( context . app . token . app_id ) . to . be . equal ( appId ) ;
169+ expect ( context . app . token . sub ) . to . be . equal ( appId ) ;
170+ expect ( context . app . token . aud ) . to . be . deep . equal ( [ `projects/${ projectId } ` ] ) ;
171+ expect ( context . auth ) . to . be . undefined ;
172+ expect ( context . instanceIdToken ) . to . be . undefined ;
173+ }
174+
175+ function checkAuthRequest (
176+ request : CallableRequest ,
177+ projectId : string ,
178+ userId : string
179+ ) {
180+ expect ( request . auth ) . to . not . be . undefined ;
181+ expect ( request . auth ) . to . not . be . null ;
182+ expect ( request . auth . uid ) . to . equal ( userId ) ;
183+ expect ( request . auth . token . uid ) . to . equal ( userId ) ;
184+ expect ( request . auth . token . sub ) . to . equal ( userId ) ;
185+ expect ( request . auth . token . aud ) . to . equal ( projectId ) ;
186+ expect ( request . instanceIdToken ) . to . be . undefined ;
187+ }
188+
189+ function checkAppCheckRequest (
190+ request : CallableRequest ,
191+ projectId : string ,
192+ appId : string
193+ ) {
194+ expect ( request . app ) . to . not . be . undefined ;
195+ expect ( request . app ) . to . not . be . null ;
196+ expect ( request . app . appId ) . to . equal ( appId ) ;
197+ expect ( request . app . token . app_id ) . to . be . equal ( appId ) ;
198+ expect ( request . app . token . sub ) . to . be . equal ( appId ) ;
199+ expect ( request . app . token . aud ) . to . be . deep . equal ( [ `projects/${ projectId } ` ] ) ;
200+ expect ( request . auth ) . to . be . undefined ;
201+ expect ( request . instanceIdToken ) . to . be . undefined ;
202+ }
203+
136204describe ( 'onCallHandler' , ( ) => {
137205 let app : firebase . app . App ;
138206
@@ -354,23 +422,11 @@ describe('onCallHandler', () => {
354422 } ) ,
355423 expectedData : null ,
356424 callableFunction : ( data , context ) => {
357- expect ( context . auth ) . to . not . be . undefined ;
358- expect ( context . auth ) . to . not . be . null ;
359- expect ( context . auth . uid ) . to . equal ( mocks . user_id ) ;
360- expect ( context . auth . token . uid ) . to . equal ( mocks . user_id ) ;
361- expect ( context . auth . token . sub ) . to . equal ( mocks . user_id ) ;
362- expect ( context . auth . token . aud ) . to . equal ( projectId ) ;
363- expect ( context . instanceIdToken ) . to . be . undefined ;
425+ checkAuthContext ( context , projectId , mocks . user_id ) ;
364426 return null ;
365427 } ,
366428 callableFunction2 : ( request ) => {
367- expect ( request . auth ) . to . not . be . undefined ;
368- expect ( request . auth ) . to . not . be . null ;
369- expect ( request . auth . uid ) . to . equal ( mocks . user_id ) ;
370- expect ( request . auth . token . uid ) . to . equal ( mocks . user_id ) ;
371- expect ( request . auth . token . sub ) . to . equal ( mocks . user_id ) ;
372- expect ( request . auth . token . aud ) . to . equal ( projectId ) ;
373- expect ( request . instanceIdToken ) . to . be . undefined ;
429+ checkAuthRequest ( request , projectId , mocks . user_id ) ;
374430 return null ;
375431 } ,
376432 expectedHttpResponse : {
@@ -383,9 +439,11 @@ describe('onCallHandler', () => {
383439 } ) ;
384440
385441 it ( 'should reject bad auth' , async ( ) => {
442+ const projectId = appsNamespace ( ) . admin . options . projectId ;
443+ const idToken = generateUnsignedIdToken ( projectId ) ;
386444 await runTest ( {
387445 httpRequest : mockRequest ( null , 'application/json' , {
388- authorization : 'Bearer FAKE' ,
446+ authorization : 'Bearer ' + idToken ,
389447 } ) ,
390448 expectedData : null ,
391449 callableFunction : ( data , context ) => {
@@ -410,35 +468,17 @@ describe('onCallHandler', () => {
410468 it ( 'should handle AppCheck token' , async ( ) => {
411469 const mock = mockFetchAppCheckPublicJwks ( ) ;
412470 const projectId = appsNamespace ( ) . admin . options . projectId ;
413- const appId = '1:65211879909: web:3ae38ef1cdcb2e01fe5f0c ' ;
471+ const appId = '123: web:abc ' ;
414472 const appCheckToken = generateAppCheckToken ( projectId , appId ) ;
415473 await runTest ( {
416474 httpRequest : mockRequest ( null , 'application/json' , { appCheckToken } ) ,
417475 expectedData : null ,
418476 callableFunction : ( data , context ) => {
419- expect ( context . app ) . to . not . be . undefined ;
420- expect ( context . app ) . to . not . be . null ;
421- expect ( context . app . appId ) . to . equal ( appId ) ;
422- expect ( context . app . token . app_id ) . to . be . equal ( appId ) ;
423- expect ( context . app . token . sub ) . to . be . equal ( appId ) ;
424- expect ( context . app . token . aud ) . to . be . deep . equal ( [
425- `projects/${ projectId } ` ,
426- ] ) ;
427- expect ( context . auth ) . to . be . undefined ;
428- expect ( context . instanceIdToken ) . to . be . undefined ;
477+ checkAppCheckContext ( context , projectId , appId ) ;
429478 return null ;
430479 } ,
431480 callableFunction2 : ( request ) => {
432- expect ( request . app ) . to . not . be . undefined ;
433- expect ( request . app ) . to . not . be . null ;
434- expect ( request . app . appId ) . to . equal ( appId ) ;
435- expect ( request . app . token . app_id ) . to . be . equal ( appId ) ;
436- expect ( request . app . token . sub ) . to . be . equal ( appId ) ;
437- expect ( request . app . token . aud ) . to . be . deep . equal ( [
438- `projects/${ projectId } ` ,
439- ] ) ;
440- expect ( request . auth ) . to . be . undefined ;
441- expect ( request . instanceIdToken ) . to . be . undefined ;
481+ checkAppCheckRequest ( request , projectId , appId ) ;
442482 return null ;
443483 } ,
444484 expectedHttpResponse : {
@@ -451,10 +491,11 @@ describe('onCallHandler', () => {
451491 } ) ;
452492
453493 it ( 'should reject bad AppCheck token' , async ( ) => {
494+ const projectId = appsNamespace ( ) . admin . options . projectId ;
495+ const appId = '123:web:abc' ;
496+ const appCheckToken = generateUnsignedAppCheckToken ( projectId , appId ) ;
454497 await runTest ( {
455- httpRequest : mockRequest ( null , 'application/json' , {
456- appCheckToken : 'FAKE' ,
457- } ) ,
498+ httpRequest : mockRequest ( null , 'application/json' , { appCheckToken } ) ,
458499 expectedData : null ,
459500 callableFunction : ( data , context ) => {
460501 return ;
@@ -545,6 +586,66 @@ describe('onCallHandler', () => {
545586 } ,
546587 } ) ;
547588 } ) ;
589+
590+ describe ( 'skip token verification debug mode support' , ( ) => {
591+ before ( ( ) => {
592+ sinon
593+ . stub ( debug , 'isDebugFeatureEnabled' )
594+ . withArgs ( 'skipTokenVerification' )
595+ . returns ( true ) ;
596+ } ) ;
597+
598+ after ( ( ) => {
599+ sinon . verifyAndRestore ( ) ;
600+ } ) ;
601+
602+ it ( 'should skip auth token verification' , async ( ) => {
603+ const projectId = appsNamespace ( ) . admin . options . projectId ;
604+ const idToken = generateUnsignedIdToken ( projectId ) ;
605+ await runTest ( {
606+ httpRequest : mockRequest ( null , 'application/json' , {
607+ authorization : 'Bearer ' + idToken ,
608+ } ) ,
609+ expectedData : null ,
610+ callableFunction : ( data , context ) => {
611+ checkAuthContext ( context , projectId , mocks . user_id ) ;
612+ return null ;
613+ } ,
614+ callableFunction2 : ( request ) => {
615+ checkAuthRequest ( request , projectId , mocks . user_id ) ;
616+ return null ;
617+ } ,
618+ expectedHttpResponse : {
619+ status : 200 ,
620+ headers : expectedResponseHeaders ,
621+ body : { result : null } ,
622+ } ,
623+ } ) ;
624+ } ) ;
625+
626+ it ( 'should skip app check token verification' , async ( ) => {
627+ const projectId = appsNamespace ( ) . admin . options . projectId ;
628+ const appId = '123:web:abc' ;
629+ const appCheckToken = generateUnsignedAppCheckToken ( projectId , appId ) ;
630+ await runTest ( {
631+ httpRequest : mockRequest ( null , 'application/json' , { appCheckToken } ) ,
632+ expectedData : null ,
633+ callableFunction : ( data , context ) => {
634+ checkAppCheckContext ( context , projectId , appId ) ;
635+ return null ;
636+ } ,
637+ callableFunction2 : ( request ) => {
638+ checkAppCheckRequest ( request , projectId , appId ) ;
639+ return null ;
640+ } ,
641+ expectedHttpResponse : {
642+ status : 200 ,
643+ headers : expectedResponseHeaders ,
644+ body : { result : null } ,
645+ } ,
646+ } ) ;
647+ } ) ;
648+ } ) ;
548649} ) ;
549650
550651describe ( 'encoding/decoding' , ( ) => {
@@ -670,3 +771,36 @@ describe('encoding/decoding', () => {
670771 expect ( https . encode ( ( ) => 'foo' ) ) . to . deep . equal ( { } ) ;
671772 } ) ;
672773} ) ;
774+
775+ describe ( 'decode tokens' , ( ) => {
776+ const projectId = 'myproject' ;
777+ const appId = '123:web:abc' ;
778+
779+ it ( 'decodes valid Auth ID Token' , ( ) => {
780+ const idToken = unsafeDecodeIdToken ( generateIdToken ( projectId ) ) ;
781+ expect ( idToken . uid ) . to . equal ( mocks . user_id ) ;
782+ expect ( idToken . sub ) . to . equal ( mocks . user_id ) ;
783+ } ) ;
784+
785+ it ( 'decodes invalid Auth ID Token' , ( ) => {
786+ const idToken = unsafeDecodeIdToken ( generateUnsignedIdToken ( projectId ) ) ;
787+ expect ( idToken . uid ) . to . equal ( mocks . user_id ) ;
788+ expect ( idToken . sub ) . to . equal ( mocks . user_id ) ;
789+ } ) ;
790+
791+ it ( 'decodes valid App Check Token' , ( ) => {
792+ const idToken = unsafeDecodeAppCheckToken (
793+ generateAppCheckToken ( projectId , appId )
794+ ) ;
795+ expect ( idToken . app_id ) . to . equal ( appId ) ;
796+ expect ( idToken . sub ) . to . equal ( appId ) ;
797+ } ) ;
798+
799+ it ( 'decodes invalid App Check Token' , ( ) => {
800+ const idToken = unsafeDecodeAppCheckToken (
801+ generateUnsignedAppCheckToken ( projectId , appId )
802+ ) ;
803+ expect ( idToken . app_id ) . to . equal ( appId ) ;
804+ expect ( idToken . sub ) . to . equal ( appId ) ;
805+ } ) ;
806+ } ) ;
0 commit comments