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 | RevokeError — authenticated: 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.py—spec/test-vectors.json(46 cases)tests/test_behavior.py+tests/behavior_runner.py—spec/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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c87c22444119f1079cfcc3c56881a35a1a864ceb74a3b83b9f1e7440f749fbec
|
|
| MD5 |
d94c9b9d95640a346151c35394e5f48a
|
|
| BLAKE2b-256 |
6a4a41ff246820866d100640e9389b5ceef55c84c81c78eaee6e8fe59ed7ef42
|
Provenance
The following attestation bundles were made for nebula_token-1.0.1.tar.gz:
Publisher:
release.yml on nebula-token/nebula-token
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nebula_token-1.0.1.tar.gz -
Subject digest:
c87c22444119f1079cfcc3c56881a35a1a864ceb74a3b83b9f1e7440f749fbec - Sigstore transparency entry: 2294217649
- Sigstore integration time:
-
Permalink:
nebula-token/nebula-token@39833feba0fcac0ebbf8df64cd2bf146f48d05e9 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/nebula-token
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@39833feba0fcac0ebbf8df64cd2bf146f48d05e9 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9551da56c6a348275f62b42dd193cfd7a74d2a3060ed91ce98e8e8b0f9aa53e5
|
|
| MD5 |
14a3c337cd0dcb68e2820449b5ee73e9
|
|
| BLAKE2b-256 |
05b8109e98efe3dc7e7d9c8c9a5217cfafaddb30d041aaa22b22236f71c80ae0
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nebula_token-1.0.1-py3-none-any.whl -
Subject digest:
9551da56c6a348275f62b42dd193cfd7a74d2a3060ed91ce98e8e8b0f9aa53e5 - Sigstore transparency entry: 2294217729
- Sigstore integration time:
-
Permalink:
nebula-token/nebula-token@39833feba0fcac0ebbf8df64cd2bf146f48d05e9 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/nebula-token
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@39833feba0fcac0ebbf8df64cd2bf146f48d05e9 -
Trigger Event:
push
-
Statement type: