1010import java .nio .charset .StandardCharsets ;
1111
1212/**
13- * Allows storage and retrieval/removal of cookies.
13+ * Allows storage and retrieval/removal of transient cookies used during the OAuth transaction.
14+ *
15+ * <p>Each login transaction gets its own uniquely-named cookies (keyed by state value),
16+ * preventing multi-tab race conditions where concurrent logins would overwrite each other's state.</p>
1417 */
1518class TransientCookieStore {
1619
@@ -19,50 +22,91 @@ private TransientCookieStore() {}
1922
2023
2124 /**
22- * Stores a state value as a cookie on the response.
25+ * Stores a state value as a transaction-keyed cookie on the response.
26+ * The cookie name includes the state value itself, ensuring each login flow
27+ * gets its own isolated cookie (e.g., "com.auth0.state.{state_value}").
2328 *
2429 * @param response the response object to set the cookie on
2530 * @param state the value for the state cookie. If null, no cookie will be set.
2631 * @param sameSite the value for the SameSite attribute on the cookie
2732 * @param useLegacySameSiteCookie whether to set a fallback cookie or not
2833 * @param isSecureCookie whether to always set the Secure cookie attribute or not
34+ * @param cookiePath the path for the cookie
2935 */
3036 static void storeState (HttpServletResponse response , String state , SameSite sameSite , boolean useLegacySameSiteCookie , boolean isSecureCookie , String cookiePath ) {
31- store (response , StorageUtils .STATE_KEY , state , sameSite , useLegacySameSiteCookie , isSecureCookie , cookiePath );
37+ if (state == null ) {
38+ return ;
39+ }
40+ store (response , StorageUtils .transactionStateKey (state ), state , sameSite , useLegacySameSiteCookie , isSecureCookie , cookiePath );
3241 }
3342
3443 /**
35- * Stores a nonce value as a cookie on the response.
44+ * Stores a nonce value as a transaction-keyed cookie on the response.
45+ * The cookie is keyed by the state value (not the nonce), so it can be
46+ * retrieved during callback using the state parameter from the URL.
3647 *
3748 * @param response the response object to set the cookie on
3849 * @param nonce the value for the nonce cookie. If null, no cookie will be set.
50+ * @param state the state value for this transaction (used as key in cookie name)
3951 * @param sameSite the value for the SameSite attribute on the cookie
4052 * @param useLegacySameSiteCookie whether to set a fallback cookie or not
4153 * @param isSecureCookie whether to always set the Secure cookie attribute or not
54+ * @param cookiePath the path for the cookie
4255 */
43- static void storeNonce (HttpServletResponse response , String nonce , SameSite sameSite , boolean useLegacySameSiteCookie , boolean isSecureCookie , String cookiePath ) {
44- store (response , StorageUtils .NONCE_KEY , nonce , sameSite , useLegacySameSiteCookie , isSecureCookie , cookiePath );
56+ static void storeNonce (HttpServletResponse response , String nonce , String state , SameSite sameSite , boolean useLegacySameSiteCookie , boolean isSecureCookie , String cookiePath ) {
57+ if (nonce == null || state == null ) {
58+ return ;
59+ }
60+ store (response , StorageUtils .transactionNonceKey (state ), nonce , sameSite , useLegacySameSiteCookie , isSecureCookie , cookiePath );
4561 }
4662
4763 /**
48- * Gets the value associated with the state cookie and removes it.
64+ * Gets the value associated with the state cookie for this transaction and removes it.
65+ * Uses the state parameter from the callback request to look up the correct transaction cookie.
66+ * Falls back to the legacy fixed-name cookie for backward compatibility during rolling upgrades.
4967 *
5068 * @param request the request object
5169 * @param response the response object
5270 * @return the value of the state cookie, if it exists
5371 */
5472 static String getState (HttpServletRequest request , HttpServletResponse response ) {
73+ String stateParam = request .getParameter ("state" );
74+ if (stateParam == null ) {
75+ return null ;
76+ }
77+
78+ // Try transaction-keyed cookie first (new behavior)
79+ String value = getOnce (StorageUtils .transactionStateKey (stateParam ), request , response );
80+ if (value != null ) {
81+ return value ;
82+ }
83+
84+ // Fallback: legacy fixed-name cookie (for in-flight transactions during upgrade from v1)
5585 return getOnce (StorageUtils .STATE_KEY , request , response );
5686 }
5787
5888 /**
59- * Gets the value associated with the nonce cookie and removes it.
89+ * Gets the value associated with the nonce cookie for this transaction and removes it.
90+ * Uses the state parameter to look up the correct transaction-keyed nonce cookie.
91+ * Falls back to the legacy fixed-name cookie for backward compatibility.
6092 *
6193 * @param request the request object
6294 * @param response the response object
95+ * @param state the state value from the callback (used to find the correct nonce cookie)
6396 * @return the value of the nonce cookie, if it exists
6497 */
65- static String getNonce (HttpServletRequest request , HttpServletResponse response ) {
98+ static String getNonce (HttpServletRequest request , HttpServletResponse response , String state ) {
99+ if (state == null ) {
100+ return null ;
101+ }
102+
103+ // Try transaction-keyed cookie first (new behavior)
104+ String value = getOnce (StorageUtils .transactionNonceKey (state ), request , response );
105+ if (value != null ) {
106+ return value ;
107+ }
108+
109+ // Fallback: legacy fixed-name cookie (for in-flight transactions during upgrade from v1)
66110 return getOnce (StorageUtils .NONCE_KEY , request , response );
67111 }
68112
0 commit comments