pub enum AuthorizationCodeState {
Issued,
Consumed {
access_token: Option<String>,
refresh_token: Option<String>,
},
Replayed {
access_token: Option<String>,
refresh_token: Option<String>,
},
}Expand description
What an issued authorization code became, once redeemed.
Consumed codes are RETAINED until their expiry rather than deleted, because RFC 6749 section
4.1.2 and RFC 9700 section 4.1.1 want a replayed code to revoke the tokens it already minted.
Deleting the record on redemption would make a replay indistinguishable from a typo, and the
stolen access token would stay live.
Debug is hand-written (see below), for the same reason as crate::client::ClientAuth: the
Consumed variant carries the access and refresh tokens this code minted, and those are bearer
credentials that a host’s tracing::debug!(?record) must not write to a log in plaintext.
Variants§
Issued
Issued and not yet redeemed.
Consumed
Already redeemed, recording what it minted so a replay can revoke it.
Fields
access_token: Option<String>The access token issued, if the issuance got as far as producing one.
None means the code was marked consumed and the issuance that followed did not
complete. That is deliberate and it is not a lost write: the record is written BEFORE
issuance precisely so that a store failure halfway through a redemption cannot take the
replay alarm offline with it (see AuthorizationServer::authorization_code_token).
There is genuinely nothing to revoke in that case, because nothing was issued, and a
replay of the code is still recognised as a replay.
Replayed
Consumed, AND presented again afterwards. A detected replay, recorded DURABLY.
§Why this is a state and not a boolean on the side
It exists to be read by a redemption that is still running. The interleaving it closes:
redeemer A takes the code, writes Consumed { access_token: None, .. } before issuing (so
that a store failure cannot disarm the alarm), and then SUSPENDS on the host’s
crate::jwt::Es256Signer, which is a network round trip when that signer fronts a KMS.
Replayer B arrives in that window, finds Consumed { access_token: None }, and correctly
concludes there is nothing to revoke, because nothing has been issued YET. B refuses the
replay and puts the record back.
If what B puts back is Consumed, it is byte for byte what A wrote, so when A wakes and
records what it minted, A cannot tell that anything happened. The replay was detected, the
audit event fired, and A’s freshly minted access token and refresh chain are live. The
alarm rang and nothing was contained.
Replayed is the trace A can see. A’s second write is a compare-and-swap against the
Consumed it wrote itself (see crate::store::Storage::compare_and_swap_authorization_code),
so this state makes it fail, and A undoes its own issuance.
It carries the same two fields because a THIRD presentation is still a replay and must still revoke whatever is by then known to have been minted.
Implementations§
Source§impl AuthorizationCodeState
impl AuthorizationCodeState
Sourcepub fn minted(&self) -> Option<(Option<&str>, Option<&str>)>
pub fn minted(&self) -> Option<(Option<&str>, Option<&str>)>
What this code minted, for the two states that can name it.
One accessor rather than two matches at each call site: the replay path treats Consumed
and Replayed identically when deciding what to revoke, and the only difference between
them is which one a concurrent redemption is allowed to overwrite.
Trait Implementations§
Source§impl Clone for AuthorizationCodeState
impl Clone for AuthorizationCodeState
Source§fn clone(&self) -> AuthorizationCodeState
fn clone(&self) -> AuthorizationCodeState
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more