| 92 |
92 |
|
///
|
| 93 |
93 |
|
/// `tower-sessions`' `insert` is last-write-wins, so two concurrent first
|
| 94 |
94 |
|
/// requests (e.g. the user opens two tabs while not yet having a token)
|
| 95 |
|
- |
/// can each generate a fresh token and clobber each other — the first
|
| 96 |
|
- |
/// form to post then fails with a 403 because its rendered token has
|
| 97 |
|
- |
/// already been overwritten.
|
| 98 |
|
- |
///
|
| 99 |
|
- |
/// Re-check via `get` after insert: if a different token landed between
|
| 100 |
|
- |
/// our get and our insert, prefer THAT value over ours so all renders
|
| 101 |
|
- |
/// from this point forward agree. `String` is `Clone`-cheap, and the
|
| 102 |
|
- |
/// race window is small enough that the duplicate insert is negligible.
|
|
95 |
+ |
/// can each generate a fresh token and clobber each other at store-save time —
|
|
96 |
+ |
/// the losing tab's rendered token is then stale and its first mutation gets a
|
|
97 |
+ |
/// 403, after which a reload picks up the winning token. This is a rare,
|
|
98 |
+ |
/// self-correcting edge for the brief window before a session has any token; it
|
|
99 |
+ |
/// cannot be reconciled in-process because each request holds an independent
|
|
100 |
+ |
/// session load (see the note in the body).
|
| 103 |
101 |
|
pub async fn get_or_create_token(session: &Session) -> Result<String, AppError> {
|
| 104 |
102 |
|
if let Some(token) = session
|
| 105 |
103 |
|
.get::<String>(CSRF_SESSION_KEY)
|
| 115 |
113 |
|
.await
|
| 116 |
114 |
|
.context("session insert")?;
|
| 117 |
115 |
|
|
| 118 |
|
- |
// Re-fetch so a concurrent insert wins consistently — whichever caller
|
| 119 |
|
- |
// wrote last is what every subsequent render will see, and we hand
|
| 120 |
|
- |
// back the same value here.
|
| 121 |
|
- |
let final_token: String = session
|
| 122 |
|
- |
.get(CSRF_SESSION_KEY)
|
| 123 |
|
- |
.await
|
| 124 |
|
- |
.context("session error")?
|
| 125 |
|
- |
.unwrap_or(candidate);
|
| 126 |
|
- |
|
| 127 |
|
- |
Ok(final_token)
|
|
116 |
+ |
// Return the token we just inserted. (A prior version re-read the key here,
|
|
117 |
+ |
// claiming to reconcile a concurrent insert — but two concurrent
|
|
118 |
+ |
// first-requests sharing a cookie each hold an independent session load, so
|
|
119 |
+ |
// the re-read only ever observes THIS request's own insert and returned
|
|
120 |
+ |
// `candidate` unconditionally. The store reconciles the tabs last-writer-wins
|
|
121 |
+ |
// at save time; a losing tab may hit one 403 and retry. The re-read was a
|
|
122 |
+ |
// no-op, so it's gone.)
|
|
123 |
+ |
Ok(candidate)
|
| 128 |
124 |
|
}
|
| 129 |
125 |
|
|
| 130 |
126 |
|
/// Validate a CSRF token against the session token
|