@@ -3573,6 +3573,219 @@ describe('/account/login', () => {
35733573 }
35743574 ) ;
35753575 } ) ;
3576+
3577+ describe ( 'skip for seen user agent' , ( ) => {
3578+ beforeEach ( ( ) => {
3579+ config . securityHistory . ipProfiling = { } ;
3580+ config . signinConfirmation . skipForNewAccounts = { enabled : false } ;
3581+ config . signinConfirmation . deviceFingerprinting = {
3582+ enabled : true ,
3583+ reportOnlyMode : false ,
3584+ duration : 604800000 // 7 days
3585+ } ;
3586+
3587+ const email = mockRequest . payload . email ;
3588+
3589+ mockDB . accountRecord = function ( ) {
3590+ return Promise . resolve ( {
3591+ authSalt : hexString ( 32 ) ,
3592+ createdAt : Date . now ( ) ,
3593+ data : hexString ( 32 ) ,
3594+ email : email ,
3595+ emailVerified : true ,
3596+ primaryEmail : {
3597+ normalizedEmail : normalizeEmail ( email ) ,
3598+ email : email ,
3599+ isVerified : true ,
3600+ isPrimary : true ,
3601+ } ,
3602+ kA : hexString ( 32 ) ,
3603+ lastAuthAt : function ( ) {
3604+ return Date . now ( ) ;
3605+ } ,
3606+ uid : uid ,
3607+ wrapWrapKb : hexString ( 32 ) ,
3608+ } ) ;
3609+ } ;
3610+
3611+ const accountRoutes = makeRoutes ( {
3612+ checkPassword : function ( ) {
3613+ return Promise . resolve ( true ) ;
3614+ } ,
3615+ config : config ,
3616+ customs : mockCustoms ,
3617+ db : mockDB ,
3618+ log : mockLog ,
3619+ mailer : mockMailer ,
3620+ push : mockPush ,
3621+ cadReminders : mockCadReminders ,
3622+ } ) ;
3623+
3624+ route = getRoute ( accountRoutes , '/account/login' ) ;
3625+ } )
3626+
3627+ it ( 'should skip verification when device is recognized and not in report-only mode' , ( ) => {
3628+ mockDB . verifiedLoginSecurityEventsByUid = sinon . spy ( ( ) =>
3629+ Promise . resolve ( [
3630+ {
3631+ name : 'account.login' ,
3632+ verified : true ,
3633+ createdAt : Date . now ( ) - 3600000 , // 1 hour ago
3634+ additionalInfo : JSON . stringify ( {
3635+ userAgent : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ,
3636+ location : { country : 'US' , state : 'CA' }
3637+ } )
3638+ }
3639+ ] )
3640+ ) ;
3641+
3642+ const requestWithUserAgent = {
3643+ ...mockRequest ,
3644+ headers : {
3645+ 'user-agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
3646+ }
3647+ } ;
3648+
3649+ return runTest ( route , requestWithUserAgent , ( response ) => {
3650+ assert . equal (
3651+ mockDB . createSessionToken . callCount ,
3652+ 1 ,
3653+ 'db.createSessionToken was called'
3654+ ) ;
3655+ const tokenData = mockDB . createSessionToken . getCall ( 0 ) . args [ 0 ] ;
3656+ assert . ok (
3657+ ! tokenData . mustVerify ,
3658+ 'sessionToken does not require verification'
3659+ ) ;
3660+ assert . ok (
3661+ response . verified ,
3662+ 'response indicates session is verified'
3663+ ) ;
3664+
3665+ assert . calledWith (
3666+ statsd . increment ,
3667+ 'account.signin.confirm.device.skip'
3668+ ) ;
3669+ } ) ;
3670+ } ) ;
3671+
3672+ it ( 'should not skip verification when device is not recognized' , ( ) => {
3673+ mockDB . verifiedLoginSecurityEventsByUid = sinon . spy ( ( ) =>
3674+ Promise . resolve ( [ ] )
3675+ ) ;
3676+
3677+ const requestWithDifferentUserAgent = {
3678+ ...mockRequest ,
3679+ headers : {
3680+ 'user-agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
3681+ }
3682+ } ;
3683+
3684+ return runTest ( route , requestWithDifferentUserAgent , ( response ) => {
3685+ assert . equal (
3686+ mockDB . createSessionToken . callCount ,
3687+ 1 ,
3688+ 'db.createSessionToken was called'
3689+ ) ;
3690+ const tokenData = mockDB . createSessionToken . getCall ( 0 ) . args [ 0 ] ;
3691+ assert . ok (
3692+ tokenData . mustVerify ,
3693+ 'sessionToken requires verification'
3694+ ) ;
3695+ assert . ok (
3696+ ! response . verified ,
3697+ 'response indicates session is not verified'
3698+ ) ;
3699+
3700+ assert . calledWith (
3701+ statsd . increment ,
3702+ 'account.signin.confirm.device.notfound'
3703+ ) ;
3704+ } ) ;
3705+ } ) ;
3706+
3707+ it ( 'should not skip verification when in report-only mode' , ( ) => {
3708+ config . signinConfirmation . deviceFingerprinting . reportOnlyMode = true ;
3709+
3710+ mockDB . verifiedLoginSecurityEventsByUid = sinon . spy ( ( ) =>
3711+ Promise . resolve ( [
3712+ {
3713+ name : 'account.login' ,
3714+ verified : true ,
3715+ createdAt : Date . now ( ) - 3600000 , // 1 hour ago
3716+ additionalInfo : JSON . stringify ( {
3717+ userAgent : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ,
3718+ location : { country : 'US' , state : 'CA' }
3719+ } )
3720+ }
3721+ ] )
3722+ ) ;
3723+
3724+ const requestWithUserAgent = {
3725+ ...mockRequest ,
3726+ headers : {
3727+ 'user-agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
3728+ }
3729+ } ;
3730+
3731+ return runTest ( route , requestWithUserAgent , ( response ) => {
3732+ assert . equal (
3733+ mockDB . createSessionToken . callCount ,
3734+ 1 ,
3735+ 'db.createSessionToken was called'
3736+ ) ;
3737+ const tokenData = mockDB . createSessionToken . getCall ( 0 ) . args [ 0 ] ;
3738+ assert . ok (
3739+ tokenData . mustVerify ,
3740+ 'sessionToken requires verification in report-only mode'
3741+ ) ;
3742+ assert . ok (
3743+ ! response . verified ,
3744+ 'response indicates session is not verified'
3745+ ) ;
3746+
3747+ // Assert StatsD metric is emitted for report-only mode
3748+ sinon . assert . calledWith (
3749+ statsd . increment ,
3750+ 'account.signin.confirm.device.match.reportOnly'
3751+ ) ;
3752+ } ) ;
3753+ } ) ;
3754+
3755+ it ( 'should handle errors gracefully and continue to existing logic' , ( ) => {
3756+ mockDB . verifiedLoginSecurityEventsByUid = sinon . spy ( ( ) =>
3757+ Promise . reject ( new Error ( 'Database connection failed' ) )
3758+ ) ;
3759+
3760+ return runTest ( route , mockRequest , ( response ) => {
3761+ assert . equal (
3762+ mockDB . createSessionToken . callCount ,
3763+ 1 ,
3764+ 'db.createSessionToken was called'
3765+ ) ;
3766+ // Should continue to existing verification logic
3767+ const tokenData = mockDB . createSessionToken . getCall ( 0 ) . args [ 0 ] ;
3768+ assert . ok (
3769+ tokenData . mustVerify ,
3770+ 'sessionToken requires verification when error occurs'
3771+ ) ;
3772+ } ) ;
3773+ } ) ;
3774+
3775+ it ( 'should not call device fingerprinting when disabled' , ( ) => {
3776+ config . signinConfirmation . deviceFingerprinting . enabled = false ;
3777+
3778+ const originalSpy = mockDB . verifiedLoginSecurityEventsByUid ;
3779+ mockDB . verifiedLoginSecurityEventsByUid = sinon . spy ( ( ) => Promise . resolve ( [ ] ) ) ;
3780+
3781+ return runTest ( route , mockRequest , ( response ) => {
3782+ // Should not call the device fingerprinting database method
3783+ assert . equal ( mockDB . verifiedLoginSecurityEventsByUid . callCount , 0 , 'device fingerprinting was not called' ) ;
3784+ // Restore original spy
3785+ mockDB . verifiedLoginSecurityEventsByUid = originalSpy ;
3786+ } ) ;
3787+ } ) ;
3788+ } ) ;
35763789 } ) ;
35773790
35783791 it ( '#integration - creating too many sessions causes an error to be logged' , ( ) => {
0 commit comments