@@ -402,6 +402,94 @@ func (ts *UserTestSuite) TestUserUpdatePassword() {
402402 }
403403}
404404
405+ func (ts * UserTestSuite ) TestUserUpdatePasswordViaRecovery () {
406+ ts .Config .Security .UpdatePasswordRequireCurrentPassword = true
407+ ts .Config .SMTP .MaxFrequency = 60
408+ u , err := models .FindUserByEmailAndAudience (ts .API .db , "test@example.com" , ts .Config .JWT .Aud )
409+ require .NoError (ts .T (), err )
410+ u .RecoverySentAt = & time.Time {}
411+ require .NoError (ts .T (), ts .API .db .Update (u ))
412+
413+ type expected struct {
414+ code int
415+ isAuthenticated bool
416+ }
417+
418+ var cases = []struct {
419+ desc string
420+ newPassword string
421+ currentPassword string
422+ recoveryType models.AuthenticationMethod
423+ expected expected
424+ }{
425+ {
426+ desc : "Current password not required in OTP recovery flow" ,
427+ newPassword : "newpassword123" ,
428+ recoveryType : models .OTP ,
429+ expected : expected {code : http .StatusOK , isAuthenticated : true },
430+ },
431+ {
432+ desc : "Current password not required in magiclink recovery flow" ,
433+ newPassword : "newpassword456" ,
434+ recoveryType : models .MagicLink ,
435+ expected : expected {code : http .StatusOK , isAuthenticated : true },
436+ },
437+ {
438+ desc : "Current password required for any other claim" ,
439+ newPassword : "newpassword456" ,
440+ recoveryType : models .EmailChange ,
441+ expected : expected {code : http .StatusBadRequest , isAuthenticated : true },
442+ },
443+ }
444+
445+ for _ , c := range cases {
446+ ts .Run (c .desc , func () {
447+ require .NoError (ts .T (), models .ClearAllOneTimeTokensForUser (ts .API .db , u .ID ))
448+
449+ // Create a session
450+ session , err := models .NewSession (u .ID , nil )
451+ require .NoError (ts .T (), err )
452+ require .NoError (ts .T (), ts .API .db .Create (session ))
453+
454+ // Add AMR claim to session to simulate recovery flow
455+ require .NoError (ts .T (), models .AddClaimToSession (ts .API .db , session .ID , c .recoveryType ))
456+
457+ // Reload session with AMR claims
458+ session , err = models .FindSessionByID (ts .API .db , session .ID , true )
459+ require .NoError (ts .T (), err )
460+ require .NotEmpty (ts .T (), session .AMRClaims , "Session should have AMR claims" )
461+
462+ // Generate access token with the recovery authentication method
463+ req := httptest .NewRequest (http .MethodPut , "http://localhost/user" , nil )
464+ token , _ , err := ts .API .generateAccessToken (req , ts .API .db , u , & session .ID , c .recoveryType )
465+ require .NoError (ts .T (), err )
466+
467+ // Update password without current password
468+ userUpdateBody := map [string ]string {"password" : c .newPassword }
469+ var buffer bytes.Buffer
470+ require .NoError (ts .T (), json .NewEncoder (& buffer ).Encode (userUpdateBody ))
471+
472+ req = httptest .NewRequest (http .MethodPut , "http://localhost/user" , & buffer )
473+ req .Header .Set ("Content-Type" , "application/json" )
474+ req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , token ))
475+
476+ // Setup response recorder
477+ w := httptest .NewRecorder ()
478+ ts .API .handler .ServeHTTP (w , req )
479+ require .Equal (ts .T (), c .expected .code , w .Code )
480+
481+ // Verify password was updated
482+ u , err = models .FindUserByEmailAndAudience (ts .API .db , "test@example.com" , ts .Config .JWT .Aud )
483+ require .NoError (ts .T (), err )
484+
485+ isAuthenticated , _ , err := u .Authenticate (context .Background (), ts .API .db , c .newPassword , ts .API .config .Security .DBEncryption .DecryptionKeys , ts .API .config .Security .DBEncryption .Encrypt , ts .API .config .Security .DBEncryption .EncryptionKeyID )
486+ require .NoError (ts .T (), err )
487+
488+ require .Equal (ts .T (), c .expected .isAuthenticated , isAuthenticated )
489+ })
490+ }
491+ }
492+
405493func (ts * UserTestSuite ) TestUserUpdatePasswordNoReauthenticationRequired () {
406494 ts .Config .Security .UpdatePasswordRequireCurrentPassword = false
407495 u , err := models .FindUserByEmailAndAudience (ts .API .db , "test@example.com" , ts .Config .JWT .Aud )
0 commit comments