@@ -2406,3 +2406,298 @@ describe('facebook limited auth adapter', () => {
2406
2406
}
2407
2407
} ) ;
2408
2408
} ) ;
2409
+
2410
+ describe ( 'OTP TOTP auth adatper' , ( ) => {
2411
+ const headers = {
2412
+ 'Content-Type' : 'application/json' ,
2413
+ 'X-Parse-Application-Id' : 'test' ,
2414
+ 'X-Parse-REST-API-Key' : 'rest' ,
2415
+ } ;
2416
+ beforeEach ( async ( ) => {
2417
+ await reconfigureServer ( {
2418
+ auth : {
2419
+ mfa : {
2420
+ enabled : true ,
2421
+ options : [ 'TOTP' ] ,
2422
+ algorithm : 'SHA1' ,
2423
+ digits : 6 ,
2424
+ period : 30 ,
2425
+ } ,
2426
+ } ,
2427
+ } ) ;
2428
+ } ) ;
2429
+
2430
+ it ( 'can enroll' , async ( ) => {
2431
+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2432
+ const OTPAuth = require ( 'otpauth' ) ;
2433
+ const secret = new OTPAuth . Secret ( ) ;
2434
+ const totp = new OTPAuth . TOTP ( {
2435
+ algorithm : 'SHA1' ,
2436
+ digits : 6 ,
2437
+ period : 30 ,
2438
+ secret,
2439
+ } ) ;
2440
+ const token = totp . generate ( ) ;
2441
+ await user . save (
2442
+ { authData : { mfa : { secret : secret . base32 , token } } } ,
2443
+ { sessionToken : user . getSessionToken ( ) }
2444
+ ) ;
2445
+ const response = user . get ( 'authDataResponse' ) ;
2446
+ expect ( response . mfa ) . toBeDefined ( ) ;
2447
+ expect ( response . mfa . recovery ) . toBeDefined ( ) ;
2448
+ expect ( response . mfa . recovery . length ) . toEqual ( 2 ) ;
2449
+ await user . fetch ( ) ;
2450
+ expect ( user . get ( 'authData' ) . mfa ) . toEqual ( { enabled : true } ) ;
2451
+ } ) ;
2452
+
2453
+ it ( 'can login with valid token' , async ( ) => {
2454
+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2455
+ const OTPAuth = require ( 'otpauth' ) ;
2456
+ const secret = new OTPAuth . Secret ( ) ;
2457
+ const totp = new OTPAuth . TOTP ( {
2458
+ algorithm : 'SHA1' ,
2459
+ digits : 6 ,
2460
+ period : 30 ,
2461
+ secret,
2462
+ } ) ;
2463
+ const token = totp . generate ( ) ;
2464
+ await user . save (
2465
+ { authData : { mfa : { secret : secret . base32 , token } } } ,
2466
+ { sessionToken : user . getSessionToken ( ) }
2467
+ ) ;
2468
+ const response = await request ( {
2469
+ headers,
2470
+ method : 'POST' ,
2471
+ url : 'http://localhost:8378/1/login' ,
2472
+ body : JSON . stringify ( {
2473
+ username : 'username' ,
2474
+ password : 'password' ,
2475
+ authData : {
2476
+ mfa : totp . generate ( ) ,
2477
+ } ,
2478
+ } ) ,
2479
+ } ) . then ( res => res . data ) ;
2480
+ expect ( response . objectId ) . toEqual ( user . id ) ;
2481
+ expect ( response . sessionToken ) . toBeDefined ( ) ;
2482
+ expect ( response . authData ) . toEqual ( { mfa : { enabled : true } } ) ;
2483
+ expect ( Object . keys ( response ) . sort ( ) ) . toEqual (
2484
+ [
2485
+ 'objectId' ,
2486
+ 'username' ,
2487
+ 'createdAt' ,
2488
+ 'updatedAt' ,
2489
+ 'authData' ,
2490
+ 'ACL' ,
2491
+ 'sessionToken' ,
2492
+ 'authDataResponse' ,
2493
+ ] . sort ( )
2494
+ ) ;
2495
+ } ) ;
2496
+
2497
+ it ( 'can change OTP with valid token' , async ( ) => {
2498
+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2499
+ const OTPAuth = require ( 'otpauth' ) ;
2500
+ const secret = new OTPAuth . Secret ( ) ;
2501
+ const totp = new OTPAuth . TOTP ( {
2502
+ algorithm : 'SHA1' ,
2503
+ digits : 6 ,
2504
+ period : 30 ,
2505
+ secret,
2506
+ } ) ;
2507
+ const token = totp . generate ( ) ;
2508
+ await user . save (
2509
+ { authData : { mfa : { secret : secret . base32 , token } } } ,
2510
+ { sessionToken : user . getSessionToken ( ) }
2511
+ ) ;
2512
+
2513
+ const new_secret = new OTPAuth . Secret ( ) ;
2514
+ const new_totp = new OTPAuth . TOTP ( {
2515
+ algorithm : 'SHA1' ,
2516
+ digits : 6 ,
2517
+ period : 30 ,
2518
+ secret : new_secret ,
2519
+ } ) ;
2520
+ const new_token = new_totp . generate ( ) ;
2521
+ await user . save (
2522
+ {
2523
+ authData : { mfa : { secret : new_secret . base32 , token : new_token , old : totp . generate ( ) } } ,
2524
+ } ,
2525
+ { sessionToken : user . getSessionToken ( ) }
2526
+ ) ;
2527
+ await user . fetch ( { useMasterKey : true } ) ;
2528
+ expect ( user . get ( 'authData' ) . mfa . secret ) . toEqual ( new_secret . base32 ) ;
2529
+ } ) ;
2530
+
2531
+ it ( 'future logins require TOTP token' , async ( ) => {
2532
+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2533
+ const OTPAuth = require ( 'otpauth' ) ;
2534
+ const secret = new OTPAuth . Secret ( ) ;
2535
+ const totp = new OTPAuth . TOTP ( {
2536
+ algorithm : 'SHA1' ,
2537
+ digits : 6 ,
2538
+ period : 30 ,
2539
+ secret,
2540
+ } ) ;
2541
+ const token = totp . generate ( ) ;
2542
+ await user . save (
2543
+ { authData : { mfa : { secret : secret . base32 , token } } } ,
2544
+ { sessionToken : user . getSessionToken ( ) }
2545
+ ) ;
2546
+ await expectAsync ( Parse . User . logIn ( 'username' , 'password' ) ) . toBeRejectedWith (
2547
+ new Parse . Error ( Parse . Error . OTHER_CAUSE , 'Missing additional authData mfa' )
2548
+ ) ;
2549
+ } ) ;
2550
+
2551
+ it ( 'future logins reject incorrect TOTP token' , async ( ) => {
2552
+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2553
+ const OTPAuth = require ( 'otpauth' ) ;
2554
+ const secret = new OTPAuth . Secret ( ) ;
2555
+ const totp = new OTPAuth . TOTP ( {
2556
+ algorithm : 'SHA1' ,
2557
+ digits : 6 ,
2558
+ period : 30 ,
2559
+ secret,
2560
+ } ) ;
2561
+ const token = totp . generate ( ) ;
2562
+ await user . save (
2563
+ { authData : { mfa : { secret : secret . base32 , token } } } ,
2564
+ { sessionToken : user . getSessionToken ( ) }
2565
+ ) ;
2566
+ await expectAsync (
2567
+ request ( {
2568
+ headers,
2569
+ method : 'POST' ,
2570
+ url : 'http://localhost:8378/1/login' ,
2571
+ body : JSON . stringify ( {
2572
+ username : 'username' ,
2573
+ password : 'password' ,
2574
+ authData : {
2575
+ mfa : 'abcd' ,
2576
+ } ,
2577
+ } ) ,
2578
+ } ) . catch ( e => {
2579
+ throw e . data ;
2580
+ } )
2581
+ ) . toBeRejectedWith ( { code : Parse . Error . SCRIPT_FAILED , error : 'Invalid MFA token' } ) ;
2582
+ } ) ;
2583
+ } ) ;
2584
+
2585
+ describe ( 'OTP SMS auth adatper' , ( ) => {
2586
+ const headers = {
2587
+ 'Content-Type' : 'application/json' ,
2588
+ 'X-Parse-Application-Id' : 'test' ,
2589
+ 'X-Parse-REST-API-Key' : 'rest' ,
2590
+ } ;
2591
+ let code ;
2592
+ let mobile ;
2593
+ const mfa = {
2594
+ enabled : true ,
2595
+ options : [ 'SMS' ] ,
2596
+ sendSMS ( smsCode , number ) {
2597
+ expect ( smsCode ) . toBeDefined ( ) ;
2598
+ expect ( number ) . toBeDefined ( ) ;
2599
+ expect ( smsCode . length ) . toEqual ( 6 ) ;
2600
+ code = smsCode ;
2601
+ mobile = number ;
2602
+ } ,
2603
+ digits : 6 ,
2604
+ period : 30 ,
2605
+ } ;
2606
+ beforeEach ( async ( ) => {
2607
+ code = '' ;
2608
+ mobile = '' ;
2609
+ await reconfigureServer ( {
2610
+ auth : {
2611
+ mfa,
2612
+ } ,
2613
+ } ) ;
2614
+ } ) ;
2615
+
2616
+ it ( 'can enroll' , async ( ) => {
2617
+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2618
+ const sessionToken = user . getSessionToken ( ) ;
2619
+ const spy = spyOn ( mfa , 'sendSMS' ) . and . callThrough ( ) ;
2620
+ await user . save ( { authData : { mfa : { mobile : '+11111111111' } } } , { sessionToken } ) ;
2621
+ await user . fetch ( { sessionToken } ) ;
2622
+ expect ( user . get ( 'authData' ) ) . toEqual ( { mfa : { enabled : false } } ) ;
2623
+ expect ( spy ) . toHaveBeenCalledWith ( code , '+11111111111' ) ;
2624
+ await user . fetch ( { useMasterKey : true } ) ;
2625
+ const authData = user . get ( 'authData' ) . mfa ?. pending ;
2626
+ expect ( authData ) . toBeDefined ( ) ;
2627
+ expect ( authData [ '+11111111111' ] ) . toBeDefined ( ) ;
2628
+ expect ( Object . keys ( authData [ '+11111111111' ] ) ) . toEqual ( [ 'token' , 'expiry' ] ) ;
2629
+
2630
+ await user . save ( { authData : { mfa : { mobile, token : code } } } , { sessionToken } ) ;
2631
+ await user . fetch ( { sessionToken } ) ;
2632
+ expect ( user . get ( 'authData' ) ) . toEqual ( { mfa : { enabled : true } } ) ;
2633
+ } ) ;
2634
+
2635
+ it ( 'future logins require SMS code' , async ( ) => {
2636
+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2637
+ const spy = spyOn ( mfa , 'sendSMS' ) . and . callThrough ( ) ;
2638
+ await user . save (
2639
+ { authData : { mfa : { mobile : '+11111111111' } } } ,
2640
+ { sessionToken : user . getSessionToken ( ) }
2641
+ ) ;
2642
+
2643
+ await user . save (
2644
+ { authData : { mfa : { mobile, token : code } } } ,
2645
+ { sessionToken : user . getSessionToken ( ) }
2646
+ ) ;
2647
+
2648
+ spy . calls . reset ( ) ;
2649
+
2650
+ await expectAsync ( Parse . User . logIn ( 'username' , 'password' ) ) . toBeRejectedWith (
2651
+ new Parse . Error ( Parse . Error . OTHER_CAUSE , 'Missing additional authData mfa' )
2652
+ ) ;
2653
+ const res = await request ( {
2654
+ headers,
2655
+ method : 'POST' ,
2656
+ url : 'http://localhost:8378/1/login' ,
2657
+ body : JSON . stringify ( {
2658
+ username : 'username' ,
2659
+ password : 'password' ,
2660
+ authData : {
2661
+ mfa : true ,
2662
+ } ,
2663
+ } ) ,
2664
+ } ) . catch ( e => e . data ) ;
2665
+ expect ( res ) . toEqual ( { code : Parse . Error . SCRIPT_FAILED , error : 'Please enter the token' } ) ;
2666
+ expect ( spy ) . toHaveBeenCalledWith ( code , '+11111111111' ) ;
2667
+ const response = await request ( {
2668
+ headers,
2669
+ method : 'POST' ,
2670
+ url : 'http://localhost:8378/1/login' ,
2671
+ body : JSON . stringify ( {
2672
+ username : 'username' ,
2673
+ password : 'password' ,
2674
+ authData : {
2675
+ mfa : code ,
2676
+ } ,
2677
+ } ) ,
2678
+ } ) . then ( res => res . data ) ;
2679
+ expect ( response . objectId ) . toEqual ( user . id ) ;
2680
+ expect ( response . sessionToken ) . toBeDefined ( ) ;
2681
+ expect ( response . authData ) . toEqual ( { mfa : { enabled : true } } ) ;
2682
+ expect ( Object . keys ( response ) . sort ( ) ) . toEqual (
2683
+ [
2684
+ 'objectId' ,
2685
+ 'username' ,
2686
+ 'createdAt' ,
2687
+ 'updatedAt' ,
2688
+ 'authData' ,
2689
+ 'ACL' ,
2690
+ 'sessionToken' ,
2691
+ 'authDataResponse' ,
2692
+ ] . sort ( )
2693
+ ) ;
2694
+ } ) ;
2695
+
2696
+ it ( 'partially enrolled users can still login' , async ( ) => {
2697
+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2698
+ await user . save ( { authData : { mfa : { mobile : '+11111111111' } } } ) ;
2699
+ const spy = spyOn ( mfa , 'sendSMS' ) . and . callThrough ( ) ;
2700
+ await Parse . User . logIn ( 'username' , 'password' ) ;
2701
+ expect ( spy ) . not . toHaveBeenCalled ( ) ;
2702
+ } ) ;
2703
+ } ) ;
0 commit comments