1
1
import { expect } from 'chai' ;
2
2
import * as express from 'express' ;
3
3
import * as firebase from 'firebase-admin' ;
4
+ import * as sinon from 'sinon' ;
4
5
5
6
import { apps as appsNamespace } from '../../../src/apps' ;
6
7
import * as https from '../../../src/common/providers/https' ;
8
+ import * as debug from '../../../src/common/debug' ;
7
9
import * as mocks from '../../fixtures/credential/key.json' ;
8
10
import {
9
11
expectedResponseHeaders ,
10
12
generateAppCheckToken ,
11
13
generateIdToken ,
14
+ generateUnsignedAppCheckToken ,
15
+ generateUnsignedIdToken ,
12
16
mockFetchAppCheckPublicJwks ,
13
17
mockFetchPublicKeys ,
14
18
mockRequest ,
15
19
} from '../../fixtures/mockrequest' ;
20
+ import {
21
+ CallableContext ,
22
+ CallableRequest ,
23
+ unsafeDecodeAppCheckToken ,
24
+ unsafeDecodeIdToken ,
25
+ } from '../../../src/common/providers/https' ;
16
26
17
27
/**
18
28
* RunHandlerResult contains the data from an express.Response.
@@ -133,6 +143,64 @@ async function runTest(test: CallTest): Promise<any> {
133
143
expect ( responseV2 . status ) . to . equal ( test . expectedHttpResponse . status ) ;
134
144
}
135
145
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
+
136
204
describe ( 'onCallHandler' , ( ) => {
137
205
let app : firebase . app . App ;
138
206
@@ -354,23 +422,11 @@ describe('onCallHandler', () => {
354
422
} ) ,
355
423
expectedData : null ,
356
424
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 ) ;
364
426
return null ;
365
427
} ,
366
428
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 ) ;
374
430
return null ;
375
431
} ,
376
432
expectedHttpResponse : {
@@ -383,9 +439,11 @@ describe('onCallHandler', () => {
383
439
} ) ;
384
440
385
441
it ( 'should reject bad auth' , async ( ) => {
442
+ const projectId = appsNamespace ( ) . admin . options . projectId ;
443
+ const idToken = generateUnsignedIdToken ( projectId ) ;
386
444
await runTest ( {
387
445
httpRequest : mockRequest ( null , 'application/json' , {
388
- authorization : 'Bearer FAKE' ,
446
+ authorization : 'Bearer ' + idToken ,
389
447
} ) ,
390
448
expectedData : null ,
391
449
callableFunction : ( data , context ) => {
@@ -410,35 +468,17 @@ describe('onCallHandler', () => {
410
468
it ( 'should handle AppCheck token' , async ( ) => {
411
469
const mock = mockFetchAppCheckPublicJwks ( ) ;
412
470
const projectId = appsNamespace ( ) . admin . options . projectId ;
413
- const appId = '1:65211879909: web:3ae38ef1cdcb2e01fe5f0c ' ;
471
+ const appId = '123: web:abc ' ;
414
472
const appCheckToken = generateAppCheckToken ( projectId , appId ) ;
415
473
await runTest ( {
416
474
httpRequest : mockRequest ( null , 'application/json' , { appCheckToken } ) ,
417
475
expectedData : null ,
418
476
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 ) ;
429
478
return null ;
430
479
} ,
431
480
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 ) ;
442
482
return null ;
443
483
} ,
444
484
expectedHttpResponse : {
@@ -451,10 +491,11 @@ describe('onCallHandler', () => {
451
491
} ) ;
452
492
453
493
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 ) ;
454
497
await runTest ( {
455
- httpRequest : mockRequest ( null , 'application/json' , {
456
- appCheckToken : 'FAKE' ,
457
- } ) ,
498
+ httpRequest : mockRequest ( null , 'application/json' , { appCheckToken } ) ,
458
499
expectedData : null ,
459
500
callableFunction : ( data , context ) => {
460
501
return ;
@@ -545,6 +586,66 @@ describe('onCallHandler', () => {
545
586
} ,
546
587
} ) ;
547
588
} ) ;
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
+ } ) ;
548
649
} ) ;
549
650
550
651
describe ( 'encoding/decoding' , ( ) => {
@@ -670,3 +771,36 @@ describe('encoding/decoding', () => {
670
771
expect ( https . encode ( ( ) => 'foo' ) ) . to . deep . equal ( { } ) ;
671
772
} ) ;
672
773
} ) ;
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