@@ -367,6 +367,90 @@ func TestUserDeleteAllSessions(t *testing.T) {
367367 require .EqualError (t , err , "401 Session no longer valid for user" )
368368}
369369
370+ // TestAuthenticateCookieOneTimeSession verifies one-time session behaviour in AuthenticateCookie:
371+ // - The session is consumed on first use and rejected on the second.
372+ // - No Set-Cookie header is ever written, including when the normal TTL-refresh branch would
373+ // fire for a regular session (sessionTimeElapsed > 10% of TTL).
374+ func TestAuthenticateCookieOneTimeSession (t * testing.T ) {
375+ ctx := base .TestCtx (t )
376+ testBucket := base .GetTestBucket (t )
377+ defer testBucket .Close (ctx )
378+ dataStore := testBucket .GetSingleDataStore ()
379+ a := NewTestAuthenticator (t , dataStore , nil , DefaultAuthenticatorOptions (ctx ))
380+
381+ const username = "Alice"
382+ user , err := a .NewUser (username , "password" , base.Set {})
383+ require .NoError (t , err )
384+ require .NoError (t , a .Save (user ))
385+
386+ t .Run ("one-time session authenticates once then is deleted" , func (t * testing.T ) {
387+ session , err := a .CreateSession (ctx , user , 2 * time .Hour , true )
388+ require .NoError (t , err )
389+
390+ req , err := http .NewRequest (http .MethodGet , "" , nil )
391+ require .NoError (t , err )
392+ req .AddCookie (a .MakeSessionCookie (session , false , false , http .SameSiteDefaultMode ))
393+
394+ authedUser , err := a .AuthenticateCookie (req , httptest .NewRecorder ())
395+ require .NoError (t , err )
396+ require .NotNil (t , authedUser )
397+ assert .Equal (t , username , authedUser .Name ())
398+
399+ _ , err = a .AuthenticateCookie (req , httptest .NewRecorder ())
400+ require .Error (t , err )
401+ assert .Equal (t , http .StatusUnauthorized , err .(* base.HTTPError ).Status )
402+ })
403+
404+ t .Run ("one-time session does not set cookie even when TTL refresh would trigger" , func (t * testing.T ) {
405+ // Expiration=now+2h with Ttl=24h means sessionTimeElapsed≈22h > tenPercentOfTtl≈2.4h,
406+ // so a regular session would call http.SetCookie here — a one-time session must not.
407+ oneTime := true
408+ sessionID , err := base .GenerateRandomSecret ()
409+ require .NoError (t , err )
410+ session := & LoginSession {
411+ ID : sessionID ,
412+ Username : username ,
413+ Expiration : time .Now ().Add (2 * time .Hour ),
414+ Ttl : 24 * time .Hour ,
415+ SessionUUID : user .GetSessionUUID (),
416+ OneTime : & oneTime ,
417+ }
418+ require .NoError (t , dataStore .Set (ctx , a .DocIDForSession (sessionID ), base .DurationToCbsExpiry (24 * time .Hour ), nil , session ))
419+
420+ req , err := http .NewRequest (http .MethodGet , "" , nil )
421+ require .NoError (t , err )
422+ req .AddCookie (& http.Cookie {Name : a .SessionCookieName , Value : sessionID })
423+ recorder := httptest .NewRecorder ()
424+ authedUser , err := a .AuthenticateCookie (req , recorder )
425+ require .NoError (t , err )
426+ require .NotNil (t , authedUser )
427+ assert .Empty (t , recorder .Header ().Get ("Set-Cookie" ), "one-time session must not set a cookie even when TTL refresh would trigger" )
428+ })
429+
430+ t .Run ("regular session refreshes cookie when TTL threshold met" , func (t * testing.T ) {
431+ // Same setup without OneTime — the refresh branch should fire and Set-Cookie should be present.
432+ sessionID , err := base .GenerateRandomSecret ()
433+ require .NoError (t , err )
434+ session := & LoginSession {
435+ ID : sessionID ,
436+ Username : username ,
437+ Expiration : time .Now ().Add (2 * time .Hour ),
438+ Ttl : 24 * time .Hour ,
439+ SessionUUID : user .GetSessionUUID (),
440+ }
441+ require .NoError (t , dataStore .Set (ctx , a .DocIDForSession (sessionID ), base .DurationToCbsExpiry (24 * time .Hour ), nil , session ))
442+
443+ req , err := http .NewRequest (http .MethodGet , "" , nil )
444+ require .NoError (t , err )
445+ req .AddCookie (& http.Cookie {Name : a .SessionCookieName , Value : sessionID })
446+ recorder := httptest .NewRecorder ()
447+ authedUser , err := a .AuthenticateCookie (req , recorder )
448+ require .NoError (t , err )
449+ require .NotNil (t , authedUser )
450+ assert .NotEmpty (t , recorder .Header ().Get ("Set-Cookie" ), "regular session should refresh cookie when TTL threshold is met" )
451+ })
452+ }
453+
370454func TestCreateOneTimeSession (t * testing.T ) {
371455 ctx := base .TestCtx (t )
372456 testBucket := base .GetTestBucket (t )
0 commit comments