nebula-token 1.0.1.pre.rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE +202 -0
- data/README.md +66 -0
- data/lib/nebula-token.rb +7 -0
- data/lib/nebula_token.rb +916 -0
- data/skills/nebula-token-ruby/SKILL.md +85 -0
- metadata +53 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nebula-token-ruby
|
|
3
|
+
description: Integrate NEBULA opaque rotating refresh tokens in a Ruby backend. Use when adding login sessions, refresh-token rotation, reuse detection, token revocation, or when replacing hand-rolled or JWT-based refresh tokens. Covers engine setup, endpoints, production store, and security rules.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Integrating nebula-token (Ruby)
|
|
7
|
+
|
|
8
|
+
NEBULA is an implementation profile of RFC 9700 (OAuth 2.0 Security BCP) for
|
|
9
|
+
refresh tokens: opaque tokens (`nbl.{kid}.{selector}.{verifier}` — pure
|
|
10
|
+
entropy, no claims), rotation on every use, reuse detection with family
|
|
11
|
+
revocation, hashed storage, optional device binding. Package: `nebula-token (RubyGems)`.
|
|
12
|
+
Normative behavior: `SPECIFICATION.md` in the repository root.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
gem install nebula-token
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Core integration
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require 'nebula_token' # 'nebula-token' also works; both load the same module
|
|
24
|
+
|
|
25
|
+
engine = NebulaToken::Engine.new(
|
|
26
|
+
peppers: { 'k1' => ENV.fetch('NEBULA_PEPPER_K1') }, # >= 32 BYTES, from env
|
|
27
|
+
active_kid: 'k1',
|
|
28
|
+
store: NebulaToken::MemoryRefreshTokenStore.new # dev only — pg store in prod
|
|
29
|
+
# reuse_grace_seconds defaults to 0 (strict); raising it costs detectability.
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Login endpoint
|
|
33
|
+
issued = engine.issue(user_id, device_id) # issued.token, .expires_at, …
|
|
34
|
+
|
|
35
|
+
# Refresh endpoint
|
|
36
|
+
result = engine.refresh(presented_token, device_id)
|
|
37
|
+
if result.ok?
|
|
38
|
+
# result.token is the NEW refresh token; the presented one is now dead
|
|
39
|
+
elsif result.error == 'CONFLICT'
|
|
40
|
+
# a concurrent refresh won the compare-and-set: nothing rotated, retry once
|
|
41
|
+
elsif %w[REUSE_DETECTED DEVICE_MISMATCH].include?(result.error)
|
|
42
|
+
# security event: family already revoked — alert, force re-login
|
|
43
|
+
# result.user_id / result.family_id identify the session, no second lookup
|
|
44
|
+
else
|
|
45
|
+
# require login
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Logout / compromise
|
|
49
|
+
revoked = engine.revoke_token(token) # authenticated: proves the verifier
|
|
50
|
+
unless revoked.ok?
|
|
51
|
+
# it REFUSED and nothing was revoked — do not report a logout ([N-36])
|
|
52
|
+
end
|
|
53
|
+
engine.revoke_family(family_id) # administrative -> Integer
|
|
54
|
+
engine.revoke_all_for_user(user_id) # administrative -> Integer
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The API is synchronous — a natural fit for Rails/Rack request lifecycles.
|
|
58
|
+
|
|
59
|
+
## Integration checklist
|
|
60
|
+
|
|
61
|
+
1. **Pepper**: generate with `openssl rand -base64 48`; store in env/KMS, never in code or DB. Key it as `k1` now so future rotation (add `k2`, switch `active_kid`) is zero-downtime.
|
|
62
|
+
2. **Store**: for production implement the six-method contract of `NebulaToken::RefreshTokenStore` (duck-typed) over your database: `find_by_selector`, `insert`, `mark_rotated`, `revoke_if_active`, `revoke_family`, `revoke_user`. `mark_rotated` and `revoke_if_active` are compare-and-set — apply the write only if the current status still matches, and return whether it applied; returning `true` unconditionally lets two concurrent refreshes fork the family. Start from `examples/pg_store.rb` (schema in `docs/STORE.md`). The in-memory store is **not for production** ([N-21]): its state is per-process and lost on restart, so reuse detection does not survive a deploy and does not work behind more than one instance.
|
|
63
|
+
3. **Login endpoint**: authenticate credentials → `issue` → set the token in an `httpOnly; Secure; SameSite=Strict` cookie scoped to the refresh path. Never return it in a JSON body read by browser JS, never localStorage.
|
|
64
|
+
4. **Refresh endpoint**: read cookie → `refresh` → on success set the NEW token in the cookie and mint a short-lived access token (5–15 min). Wrap the call in one DB transaction so insert + mark_rotated commit atomically.
|
|
65
|
+
5. **Error handling**: `result.error` (string). Every failure means "no access token issued", and failures carry `result.user_id` / `result.family_id` whenever a record was resolved — every code except `MALFORMED`, `UNKNOWN_KID` and `NOT_FOUND` — so a security event needs no second lookup of a token you were told never to log ([N-39]). The code set is open — always give the `case` an `else` that denies. `CONFLICT` is transient: retry once ([N-35]). `REUSE_DETECTED` and `DEVICE_MISMATCH` additionally mean the family is already revoked — log a security event and alert.
|
|
66
|
+
6. **Logout**: `revoke_token` (one session, requires the token itself) / `revoke_family`, `revoke_all_for_user` (administrative: password change, compromise). They do not return the same shape, and the difference is load-bearing: `revoke_token` returns a `RevokeResult` that **can refuse** (`MALFORMED`, `UNKNOWN_KID`, `NOT_FOUND`, `VERIFIER_MISMATCH`) ([N-36]) — check `.ok?` and read the count from `.revoked`; calling it and answering `204` reports a logout that never happened and leaves the session alive. The two administrative calls take no token, cannot refuse, and return the `Integer` count directly.
|
|
67
|
+
7. **Device binding (optional)**: pass a stable device identifier at issue AND every refresh. Strong on native apps (Keychain/Keystore-held ID); weaker on web where the ID travels with the token.
|
|
68
|
+
8. **GC**: run the store's `delete_expired` helper periodically; keep rotated/revoked rows until the family's absolute deadline — they power reuse detection.
|
|
69
|
+
|
|
70
|
+
## Security rules (non-negotiable)
|
|
71
|
+
|
|
72
|
+
- Never log a full token, the verifier, the pepper or the raw device identifier, and never put them in an error value ([N-14]/[N-46]); the selector is public and may be logged as a correlation id. `IssueResult`, `RefreshResult`, `ParsedToken`, `TokenRecord` and `Engine` redact the credential (and the peppers) in `inspect`/`to_s`, so `p result` and `p engine` are safe — but string-building the token yourself is not.
|
|
73
|
+
- Never store or transmit the raw verifier or raw deviceId server-side — the engine already guarantees the store only sees hashes; don't work around it.
|
|
74
|
+
- Let store errors propagate (fail closed). Never rescue a store exception and turn it into a "session expired": that would report revocations that never happened.
|
|
75
|
+
- Default TTLs (30d absolute / 7d idle) suit consumer apps; for high-assurance (NIST AAL2), configure ~12h absolute / 30min idle.
|
|
76
|
+
- `reuse_grace_seconds` **defaults to 0 (strict), and that is the recommendation** ([N-30]). A non-zero window buys retry tolerance for clients that sent a refresh and never got the response, and it costs detectability: for that many seconds after a rotation, a thief holding the rotated predecessor who acts before the legitimate client is **served a valid token**, the legitimate client is evicted with `REVOKED`, and **no `REUSE_DETECTED` is raised**. Keep it as small as your clients need, and if you enable it, alert on the `REVOKED` rate.
|
|
77
|
+
|
|
78
|
+
## Common mistakes to avoid
|
|
79
|
+
|
|
80
|
+
- Reusing the presented refresh token after a successful refresh — it is dead; always persist the returned one.
|
|
81
|
+
- Retrying a failed refresh with the same token in client retry loops — outside the grace window this burns the family (by design).
|
|
82
|
+
- Putting user data or expiry claims "inside" the token — NEBULA tokens are opaque by design; claims belong in the short-lived access token.
|
|
83
|
+
- Calling refresh from multiple concurrent requests with the same token — exactly one wins, the rest get `CONFLICT` and should retry once, never a forked family. Serialize refreshes client-side rather than relying on that, and do not raise `reuse_grace_seconds` to paper over it: the window costs detectability, and the compare-and-set already prevents the fork.
|
|
84
|
+
- Writing a store whose `mark_rotated` ignores `from_status`, or whose `revoke_family` returns a constant instead of the number of rows it changed.
|
|
85
|
+
- Deleting rotated/revoked rows before the family's absolute deadline — that silently converts every replay from `REUSE_DETECTED` into `NOT_FOUND`.
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nebula-token
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.1.pre.rc.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matteo Teodori
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Rotation, reuse detection, family revocation, sender binding. Stdlib
|
|
13
|
+
only.
|
|
14
|
+
email:
|
|
15
|
+
- hello@nebulatoken.dev
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/nebula-token.rb
|
|
24
|
+
- lib/nebula_token.rb
|
|
25
|
+
- skills/nebula-token-ruby/SKILL.md
|
|
26
|
+
homepage: https://nebulatoken.dev
|
|
27
|
+
licenses:
|
|
28
|
+
- Apache-2.0
|
|
29
|
+
metadata:
|
|
30
|
+
homepage_uri: https://nebulatoken.dev
|
|
31
|
+
source_code_uri: https://github.com/nebula-token/nebula-token/tree/main/packages/ruby
|
|
32
|
+
changelog_uri: https://github.com/nebula-token/nebula-token/blob/main/CHANGELOG.md
|
|
33
|
+
bug_tracker_uri: https://github.com/nebula-token/nebula-token/issues
|
|
34
|
+
documentation_uri: https://github.com/nebula-token/nebula-token/blob/main/SPECIFICATION.md
|
|
35
|
+
rubygems_mfa_required: 'true'
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '3.3'
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 3.6.9
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: Opaque rotating refresh tokens (RFC 9700 model).
|
|
53
|
+
test_files: []
|