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.0.tar.gz (22.1 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.0-py3-none-any.whl (22.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: nebula_token-1.0.0.tar.gz
  • Upload date:
  • Size: 22.1 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.0.tar.gz
Algorithm Hash digest
SHA256 2c537882f15591d748f91364a609b4847b3194a6f64d8bab75331e48fb228b7e
MD5 0120fa8292b92c5bf32a3c3c19d76ed3
BLAKE2b-256 f2a5bdc17eb94303836422ebec9c66f79d5f6980baaef5bdbe8534aa36ecb2d5

See more details on using hashes here.

Provenance

The following attestation bundles were made for nebula_token-1.0.0.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.0-py3-none-any.whl.

File metadata

  • Download URL: nebula_token-1.0.0-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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cfc89c01a018b261e998af9e9c49ce7f48f96106587a52d5234c095ef32cff49
MD5 0999478dc13be2060609178da2713dcf
BLAKE2b-256 0c4b9004613c6250da641eb4c2d1d156433fac6c93fd683f33139d9e5e9b7294

See more details on using hashes here.

Provenance

The following attestation bundles were made for nebula_token-1.0.0-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