Skip to content

StatelessOAuthStateStore provides no one-time-use guarantee; sample callback allows state replay / duplicate processing #1919

Description

@evanmarshall

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.pyif state_store.consume(state): ... client.oauth_v2_access(...) then installation_store.save(installation) with no idempotency guard.

How to reproduce

  1. Issue one valid state via issue().
  2. Prepare two callback requests carrying the same state (and code).
  3. Fire both concurrently, or replay the same state twice before exp.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    oauthquestionM-T: User needs support to use the project

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions