diff --git a/slack_sdk/oauth/state_store/stateless/__init__.py b/slack_sdk/oauth/state_store/stateless/__init__.py index e123b730c..3e4de7b49 100644 --- a/slack_sdk/oauth/state_store/stateless/__init__.py +++ b/slack_sdk/oauth/state_store/stateless/__init__.py @@ -11,6 +11,47 @@ class StatelessOAuthStateStore(OAuthStateStore, AsyncOAuthStateStore): + """A stateless OAuth ``state`` store backed by a signed, self-contained token. + + ``issue()`` creates an HMAC-SHA256 signed token in the JWT style. The token + carries only an expiration claim. ``consume()`` re-verifies the signature and + the expiry. Nothing is stored server-side. No storage backend is required. + + .. warning:: + This store provides **no one-time-use / anti-replay guarantee**. It returns + ``True`` for the same token every time until that token expires. This is + inherent to a stateless token. The token can only be invalidated by expiry. + It cannot be revoked early unless you add server-side state, and that would + defeat the point of being stateless. + + Consequences for integrators: + + - **Keep the token lifetime short.** A token stays valid until it expires. + Use a short ``expiration_seconds``. A value of ``300`` (5 minutes) is a + good choice and matches the OAuth sample. This limits how long a captured + ``state`` can be replayed. + - **Make the callback idempotent.** A captured ``state`` can be replayed + within its lifetime. Your callback may run more than once for a single + authorization. The ``oauth.v2.access`` exchange and the installation + persistence must tolerate this. Slack authorization codes are single-use. + A sequential replay fails the token exchange. The real exposure is a + concurrent duplicate callback. + - **CSRF protection is unchanged.** CSRF defense comes from the + ``Secure; HttpOnly`` state cookie. ``OAuthStateUtils`` validates that cookie + before ``consume()`` runs in the callback. Compared to the stateful stores, + this store only gives up anti-replay and defense-in-depth. It does not give + up CSRF protection. + + Args: + expiration_seconds: Lifetime of an issued ``state`` token in seconds. After + this window ``consume()`` returns ``False``. Keep this value short. A + value of ``300`` (5 minutes) is a good default. Avoid long lifetimes. + signing_secret: Secret used to sign and verify tokens with HMAC. Keep it + secret. Keep it the same across every instance that must validate a + shared token. + logger: Logger for validation warnings. + """ + def __init__( self, *, @@ -55,6 +96,12 @@ def issue(self, *args, **kwargs) -> str: return f"{header}.{payload}.{signature}" def consume(self, state: str) -> bool: + """Validate a ``state`` token's signature and ``expiration`` claim. + + Returns ``True`` for any correctly signed, unexpired token, **every time**. + This store keeps no record of consumed tokens. It does not enforce + one-time-use. See the class docstring for the anti-replay caveat. + """ try: parts = state.split(".") if len(parts) != 3: