@@ -2001,6 +2001,82 @@ describe('OTP TOTP auth adatper', () => {
20012001 } )
20022002 ) . toBeRejectedWith ( { code : Parse . Error . SCRIPT_FAILED , error : 'Invalid MFA token' } ) ;
20032003 } ) ;
2004+
2005+ it ( 'allows unlinking MFA without TOTP verification (by design)' , async ( ) => {
2006+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2007+ const sessionToken = user . getSessionToken ( ) ;
2008+ const OTPAuth = require ( 'otpauth' ) ;
2009+ const secret = new OTPAuth . Secret ( ) ;
2010+ const totp = new OTPAuth . TOTP ( {
2011+ algorithm : 'SHA1' ,
2012+ digits : 6 ,
2013+ period : 30 ,
2014+ secret,
2015+ } ) ;
2016+ const token = totp . generate ( ) ;
2017+ // Enable MFA
2018+ await user . save (
2019+ { authData : { mfa : { secret : secret . base32 , token } } } ,
2020+ { sessionToken }
2021+ ) ;
2022+ await user . fetch ( { useMasterKey : true } ) ;
2023+ expect ( user . get ( 'authData' ) . mfa . secret ) . toBeDefined ( ) ;
2024+ // Unlink MFA without providing TOTP
2025+ await user . save (
2026+ { authData : { mfa : null } } ,
2027+ { sessionToken }
2028+ ) ;
2029+ // MFA should be removed
2030+ await user . fetch ( { useMasterKey : true } ) ;
2031+ expect ( user . get ( 'authData' ) ) . toBeUndefined ( ) ;
2032+ // Login should succeed without MFA
2033+ const response = await request ( {
2034+ headers,
2035+ method : 'POST' ,
2036+ url : 'http://localhost:8378/1/login' ,
2037+ body : JSON . stringify ( {
2038+ username : 'username' ,
2039+ password : 'password' ,
2040+ } ) ,
2041+ } ) ;
2042+ expect ( response . data . sessionToken ) . toBeDefined ( ) ;
2043+ } ) ;
2044+
2045+ it ( 'allows blocking MFA unlink via beforeSave trigger' , async ( ) => {
2046+ Parse . Cloud . beforeSave ( '_User' , request => {
2047+ const authData = request . object . get ( 'authData' ) ;
2048+ if ( authData ?. mfa === null ) {
2049+ throw new Parse . Error ( Parse . Error . VALIDATION_ERROR , 'Cannot disable MFA without verification' ) ;
2050+ }
2051+ } ) ;
2052+ const user = await Parse . User . signUp ( 'username' , 'password' ) ;
2053+ const OTPAuth = require ( 'otpauth' ) ;
2054+ const secret = new OTPAuth . Secret ( ) ;
2055+ const totp = new OTPAuth . TOTP ( {
2056+ algorithm : 'SHA1' ,
2057+ digits : 6 ,
2058+ period : 30 ,
2059+ secret,
2060+ } ) ;
2061+ const token = totp . generate ( ) ;
2062+ // Enable MFA
2063+ await user . save (
2064+ { authData : { mfa : { secret : secret . base32 , token } } } ,
2065+ { sessionToken : user . getSessionToken ( ) }
2066+ ) ;
2067+ // Attempt to unlink MFA — should be blocked by beforeSave trigger
2068+ await expectAsync (
2069+ user . save (
2070+ { authData : { mfa : null } } ,
2071+ { sessionToken : user . getSessionToken ( ) }
2072+ )
2073+ ) . toBeRejectedWith (
2074+ new Parse . Error ( Parse . Error . VALIDATION_ERROR , 'Cannot disable MFA without verification' )
2075+ ) ;
2076+ // MFA should still be enabled
2077+ await user . fetch ( { useMasterKey : true } ) ;
2078+ expect ( user . get ( 'authData' ) . mfa . secret ) . toBeDefined ( ) ;
2079+ } ) ;
20042080} ) ;
20052081
20062082describe ( 'OTP SMS auth adatper' , ( ) => {
0 commit comments