Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions slack_sdk/oauth/state_store/stateless/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*,
Expand Down Expand Up @@ -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:
Expand Down