@@ -343,6 +343,98 @@ public void shouldNotDeleteOtherTransactionCookies() {
343343 assertThat (deletedCookies [0 ].getMaxAge (), is (0 ));
344344 }
345345
346+ // --- Nonce multi-tab isolation tests ---
347+
348+ @ Test
349+ public void shouldIsolateMultipleNonceTransactionsOnRetrieval () {
350+ // Both nonce cookies exist (two concurrent tabs)
351+ Cookie nonceA = new Cookie ("com.auth0.nonce.stateA" , "nonceA" );
352+ Cookie nonceB = new Cookie ("com.auth0.nonce.stateB" , "nonceB" );
353+ request .setCookies (nonceA , nonceB );
354+
355+ // Retrieve nonce for Tab A — should get nonceA without touching nonceB
356+ String resultA = TransientCookieStore .getNonce (request , response , "stateA" );
357+ assertThat (resultA , is ("nonceA" ));
358+
359+ // Only stateA's nonce cookie should be deleted
360+ Cookie [] deletedCookies = response .getCookies ();
361+ assertThat (deletedCookies .length , is (1 ));
362+ assertThat (deletedCookies [0 ].getName (), is ("com.auth0.nonce.stateA" ));
363+ assertThat (deletedCookies [0 ].getMaxAge (), is (0 ));
364+
365+ // Tab B can still retrieve its nonce independently (new request)
366+ MockHttpServletRequest requestB = new MockHttpServletRequest ();
367+ MockHttpServletResponse responseB = new MockHttpServletResponse ();
368+ requestB .setCookies (nonceA , nonceB );
369+
370+ String resultB = TransientCookieStore .getNonce (requestB , responseB , "stateB" );
371+ assertThat (resultB , is ("nonceB" ));
372+ }
373+
374+ @ Test
375+ public void shouldPreferTransactionKeyedNonceOverLegacyAndNotDeleteLegacy () {
376+ Cookie txCookie = new Cookie ("com.auth0.nonce.stateA" , "txNonce" );
377+ Cookie legacyCookie = new Cookie ("com.auth0.nonce" , "legacyNonce" );
378+ request .setCookies (txCookie , legacyCookie );
379+
380+ String nonce = TransientCookieStore .getNonce (request , response , "stateA" );
381+ assertThat (nonce , is ("txNonce" ));
382+
383+ // Transaction-keyed cookie was consumed; legacy cookie should NOT be deleted
384+ // (it may belong to another in-flight flow upgrading from v1)
385+ Cookie [] deletedCookies = response .getCookies ();
386+ assertThat (deletedCookies .length , is (1 ));
387+ assertThat (deletedCookies [0 ].getName (), is ("com.auth0.nonce.stateA" ));
388+ }
389+
390+ @ Test
391+ public void shouldRetrieveCorrectStateAndNonceForEachTabEndToEnd () {
392+ // Simulate two concurrent login flows storing state + nonce
393+ MockHttpServletResponse storeResponse = new MockHttpServletResponse ();
394+ TransientCookieStore .storeState (storeResponse , "stateA" , SameSite .LAX , false , false , null );
395+ TransientCookieStore .storeNonce (storeResponse , "nonceA" , "stateA" , SameSite .LAX , false , false , null );
396+ TransientCookieStore .storeState (storeResponse , "stateB" , SameSite .LAX , false , false , null );
397+ TransientCookieStore .storeNonce (storeResponse , "nonceB" , "stateB" , SameSite .LAX , false , false , null );
398+
399+ // Tab A callback
400+ MockHttpServletRequest requestA = new MockHttpServletRequest ();
401+ MockHttpServletResponse responseA = new MockHttpServletResponse ();
402+ requestA .setCookies (
403+ new Cookie ("com.auth0.state.stateA" , "stateA" ),
404+ new Cookie ("com.auth0.nonce.stateA" , "nonceA" ),
405+ new Cookie ("com.auth0.state.stateB" , "stateB" ),
406+ new Cookie ("com.auth0.nonce.stateB" , "nonceB" )
407+ );
408+ requestA .setParameter ("state" , "stateA" );
409+
410+ String stateA = TransientCookieStore .getState (requestA , responseA );
411+ String nonceA = TransientCookieStore .getNonce (requestA , responseA , "stateA" );
412+ assertThat (stateA , is ("stateA" ));
413+ assertThat (nonceA , is ("nonceA" ));
414+
415+ // Tab B callback
416+ MockHttpServletRequest requestB = new MockHttpServletRequest ();
417+ MockHttpServletResponse responseB = new MockHttpServletResponse ();
418+ requestB .setCookies (
419+ new Cookie ("com.auth0.state.stateA" , "stateA" ),
420+ new Cookie ("com.auth0.nonce.stateA" , "nonceA" ),
421+ new Cookie ("com.auth0.state.stateB" , "stateB" ),
422+ new Cookie ("com.auth0.nonce.stateB" , "nonceB" )
423+ );
424+ requestB .setParameter ("state" , "stateB" );
425+
426+ String stateB = TransientCookieStore .getState (requestB , responseB );
427+ String nonceB = TransientCookieStore .getNonce (requestB , responseB , "stateB" );
428+ assertThat (stateB , is ("stateB" ));
429+ assertThat (nonceB , is ("nonceB" ));
430+
431+ // Verify Tab A's retrieval didn't affect Tab B's cookies
432+ Cookie [] deletedByA = responseA .getCookies ();
433+ for (Cookie c : deletedByA ) {
434+ assertThat (c .getName (), not (containsString ("stateB" )));
435+ }
436+ }
437+
346438 // --- Origin domain tests ---
347439
348440 private static final String TEST_SECRET = "testClientSecret123" ;
0 commit comments