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.1rc1.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.1rc1-py3-none-any.whl (22.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: nebula_token-1.0.1rc1.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.1rc1.tar.gz
Algorithm Hash digest
SHA256 4e824147b3fce2cb1116a53a5d5624de1a3808233d9f32fe18560b78291804fd
MD5 dff87f0e5e7507f711217f16f4a468ec
BLAKE2b-256 175ddce3d06bc58a9cf351c71091ed9c2adb924148c7a165e323c5e01bfc12b4

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: nebula_token-1.0.1rc1-py3-none-any.whl
  • Upload date:
  • Size: 22.6 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.1rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 b7ee199ae84acbbdd648f4a820fa9126681126a4d8be9e0b0986881e18bd3ec6
MD5 22757fad1ac3b3ec38181f03dde02c05
BLAKE2b-256 fbd53ccd3ace5bdf54a41388a4f3f800e645bbedab5eab52c56332de80ba092d

See more details on using hashes here.

Provenance

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