What's wrong
StatelessOAuthStateStore.consume() (added in #1916) validates an OAuth state by checking its HMAC signature and exp claim, then returns True — with no consumed-state tracking, atomic claim, or nonce/jti. Because the store is stateless, a validly signed, unexpired state can be replayed any number of times within its lifetime.
The SDK's own OAuth sample then wires consume() into a callback that performs oauth_v2_access() and persists an installation with no idempotency or concurrency guard, so a replayed or concurrent duplicate callback can run the downstream token exchange and installation persistence more than once.
I want to be measured about severity: consume() returning True repeatedly is arguably inherent to a stateless design, and the concrete duplicate-processing was demonstrated via the shipped sample rather than a core library API. So this is largely an interface/guidance gap — but integrators who copy the sample inherit a weaker one-time-use / CSRF guarantee than the stateful stores provide.
Where (current main)
slack_sdk/oauth/state_store/stateless/__init__.py, consume() — verifies HMAC + exp, then return True, no one-time-use mechanism:
https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/oauth/state_store/stateless/__init__.py#L57-L75
Example integration that copies the pattern:
integration_tests/samples/oauth/oauth_v2.py — if state_store.consume(state): ... client.oauth_v2_access(...) then installation_store.save(installation) with no idempotency guard.
How to reproduce
- Issue one valid state via
issue().
- Prepare two callback requests carrying the same state (and code).
- Fire both concurrently, or replay the same state twice before
exp.
consume() returns True for both, and the downstream exchange + installation save run more than once for a single authorization.
Expected vs actual
Expected: OAuth state gives a one-time-use / anti-replay guarantee, or the docs/sample make clear it does not and require idempotent callbacks.
Actual: a signed, unexpired state validates repeatedly; the sample presents consume() as if it enforces one-time-use.
Suggested fix
- Document explicitly that
StatelessOAuthStateStore provides no replay/one-time-use protection and that callbacks using it must make downstream exchange + persistence idempotent.
- Optionally add an atomic one-time claim (short-TTL jti/nonce registry) for stateless states at the integration boundary, and guard the sample callback with an idempotency check so integrators don't copy an unsafe pattern.
Found while running Ito (AI code review, free for open source) against recently merged PRs — full analysis: https://app.ito.ai/share/e12306b0-6a0d-44af-89e3-90d064be3b26.
What's wrong
StatelessOAuthStateStore.consume()(added in #1916) validates an OAuthstateby checking its HMAC signature andexpclaim, then returnsTrue— with no consumed-state tracking, atomic claim, or nonce/jti. Because the store is stateless, a validly signed, unexpired state can be replayed any number of times within its lifetime.The SDK's own OAuth sample then wires
consume()into a callback that performsoauth_v2_access()and persists an installation with no idempotency or concurrency guard, so a replayed or concurrent duplicate callback can run the downstream token exchange and installation persistence more than once.I want to be measured about severity:
consume()returningTruerepeatedly is arguably inherent to a stateless design, and the concrete duplicate-processing was demonstrated via the shipped sample rather than a core library API. So this is largely an interface/guidance gap — but integrators who copy the sample inherit a weaker one-time-use / CSRF guarantee than the stateful stores provide.Where (current main)
slack_sdk/oauth/state_store/stateless/__init__.py,consume()— verifies HMAC +exp, thenreturn True, no one-time-use mechanism:https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/oauth/state_store/stateless/__init__.py#L57-L75
Example integration that copies the pattern:
integration_tests/samples/oauth/oauth_v2.py—if state_store.consume(state): ... client.oauth_v2_access(...)theninstallation_store.save(installation)with no idempotency guard.How to reproduce
issue().exp.consume()returnsTruefor both, and the downstream exchange + installation save run more than once for a single authorization.Expected vs actual
Expected: OAuth
stategives a one-time-use / anti-replay guarantee, or the docs/sample make clear it does not and require idempotent callbacks.Actual: a signed, unexpired state validates repeatedly; the sample presents
consume()as if it enforces one-time-use.Suggested fix
StatelessOAuthStateStoreprovides no replay/one-time-use protection and that callbacks using it must make downstream exchange + persistence idempotent.Found while running Ito (AI code review, free for open source) against recently merged PRs — full analysis: https://app.ito.ai/share/e12306b0-6a0d-44af-89e3-90d064be3b26.