Skip to main content

Opaque rotating refresh tokens (RFC 9700 model): rotation, reuse detection, family revocation, sender binding. Stdlib only.

Project description

nebula-token

Python reference implementation of NEBULA — opaque rotating refresh tokens (RFC 9700 model). Standard library only, Python ≥ 3.11. Implements spec_version = 1.

pip install nebula-token
import os
from nebula_token import NebulaEngine, MemoryRefreshTokenStore

engine = NebulaEngine(
    peppers={"k1": os.environ["NEBULA_PEPPER_K1"]},  # >= 32 BYTES, from env/KMS
    active_kid="k1",
    store=MemoryRefreshTokenStore(),  # dev only — implement RefreshTokenStore for production
    reuse_grace_seconds=0,            # see [N-30] before raising this
)

issued = engine.issue("usr_1", device_id)
# issued.token, .user_id, .family_id, .generation, .expires_at, .idle_expires_at

result = engine.refresh(issued.token, device_id)
if result.ok:
    ...            # persist result.token client-side; the presented token is dead
else:
    result.error   # "REUSE_DETECTED", "CONFLICT", … — treat an unknown code as a refusal

API

Call Returns
issue(user_id, device_id=None) IssueResult — raises NebulaConfigError for an invalid device_id
refresh(token, device_id=None) RefreshOk | RefreshError (never raises for protocol outcomes)
revoke_token(token) RevokeOk | RevokeErrorauthenticated: proves the verifier, then revokes the family and reports revoked. Takes no device identifier ([N-36])
revoke_family(family_id) int revoked — administrative, no token required
revoke_all_for_user(user_id) int revoked — administrative, no token required

Timestamps are integer Unix seconds everywhere. device_id=None (unbound) and device_id="" (bound to the empty string) are different values.

Failure results carry user_id and family_id whenever the engine resolved a record — every code except MALFORMED, UNKNOWN_KID and NOT_FOUND — so a security event can be attributed without a second lookup.

NebulaErrorCode is a closed Literal for type checking but an open set at runtime: a future minor version may add a code, so treat an unrecognised value as a refusal rather than assuming your if/elif chain is exhaustive.

Stores

RefreshTokenStore is a six-method Protocol; mark_rotated and revoke_if_active are compare-and-set and must return whether they applied — returning True unconditionally re-opens the race that forks a token family. examples/sqlite_store.py is a runnable template; the schema is in docs/STORE.md.

The contract is synchronous. Blocking drivers are the Python norm, and an engine that never awaits cannot silently discard a coroutine. Under asyncio, call the engine off the event loop:

result = await asyncio.to_thread(engine.refresh, token, device_id)

Do not give the engine a store whose methods are async def: nothing awaits them, so insert would return an un-awaited coroutine and the engine would hand out a token for a record that was never written.

Infrastructure failures (store unreachable, timeout, constraint violation) must raise; the exception propagates out of the engine and is never converted into a protocol outcome, so callers fail closed.

MemoryRefreshTokenStore is mutex-guarded and safe across threads, but is per-process and lost on restart — development and tests only.

Tests

cd packages/python
pip install pytest
pytest -q

pyproject.toml puts the source tree and tests/ on sys.path (tool.pytest.ini_options.pythonpath), so no install step is needed. The suites read the normative artifacts from spec/ in the repository root, located by walking up from the test file:

  • tests/test_conformance.pyspec/test-vectors.json (46 cases)
  • tests/test_behavior.py + tests/behavior_runner.pyspec/behavior-vectors.json (38 scenarios)
  • tests/test_engine.py — what the vectors cannot express: the constant-time guard, concurrency, store failures, configuration, and secret hygiene

Type checking: pip install mypy && mypy --strict (configured in pyproject.toml).

See the repository README and SPECIFICATION.md.

Agent skill: skills/nebula-token-python/SKILL.md teaches an AI coding assistant to integrate this package correctly. Install it with the standard agent-skill installer:

npx skills add nebula-token/nebula-token --skill nebula-token-python

That lands in the project's .claude/skills/, checked in and shared with your team; add -g for your personal ~/.claude/skills/ instead. In Claude Code it is equally a plugin — /plugin marketplace add nebula-token/nebula-token, then /plugin install nebula-token-python@nebula-token. Neither route copies anything: both read this directory in place, through .claude-plugin/marketplace.json.

As a fallback the skill also ships inside the published artefact, in the same installable shape — after pip install nebula-token the directory is at <site-packages>/nebula_token/skills/nebula-token-python/, and copying that directory into ~/.claude/skills/ is the whole installation. All ten skills and every install route are in skills/README.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nebula_token-1.0.1.tar.gz (22.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nebula_token-1.0.1-py3-none-any.whl (22.5 kB view details)

Uploaded Python 3

File details

Details for the file nebula_token-1.0.1.tar.gz.

File metadata

  • Download URL: nebula_token-1.0.1.tar.gz
  • Upload date:
  • Size: 22.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for nebula_token-1.0.1.tar.gz
Algorithm Hash digest
SHA256 c87c22444119f1079cfcc3c56881a35a1a864ceb74a3b83b9f1e7440f749fbec
MD5 d94c9b9d95640a346151c35394e5f48a
BLAKE2b-256 6a4a41ff246820866d100640e9389b5ceef55c84c81c78eaee6e8fe59ed7ef42

See more details on using hashes here.

Provenance

The following attestation bundles were made for nebula_token-1.0.1.tar.gz:

Publisher: release.yml on nebula-token/nebula-token

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nebula_token-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: nebula_token-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 22.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for nebula_token-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9551da56c6a348275f62b42dd193cfd7a74d2a3060ed91ce98e8e8b0f9aa53e5
MD5 14a3c337cd0dcb68e2820449b5ee73e9
BLAKE2b-256 05b8109e98efe3dc7e7d9c8c9a5217cfafaddb30d041aaa22b22236f71c80ae0

See more details on using hashes here.

Provenance

The following attestation bundles were made for nebula_token-1.0.1-py3-none-any.whl:

Publisher: release.yml on nebula-token/nebula-token

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page