identity: access-token TTL via logical clock — expires_in (RFC 6749 §4.2.2, +8 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 22s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 22s
The token registry holds a logical clock (advance/now; the substrate has no wall clock). Grants carry a Ttl; each access token carries an Expires (Now-at-issue + Ttl, or infinity); introspect returns inactive once Now reaches it. Refresh mints a fresh short-lived access token — short access tokens, long refresh tokens. issue/4 and issue_grant/4 default to infinity so all prior behaviour is unchanged. New tests/expiry.sx. token loop/6. 138/138. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ SUITES=(
|
||||
"cache|id-cache-test-pass|id-cache-test-count"
|
||||
"audit|id-audit-test-pass|id-audit-test-count"
|
||||
"federation|id-fed-test-pass|id-fed-test-count"
|
||||
"expiry|id-expiry-test-pass|id-expiry-test-count"
|
||||
)
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
@@ -68,6 +69,7 @@ cat > "$TMPFILE" << 'EPOCHS'
|
||||
(load "lib/identity/tests/cache.sx")
|
||||
(load "lib/identity/tests/audit.sx")
|
||||
(load "lib/identity/tests/federation.sx")
|
||||
(load "lib/identity/tests/expiry.sx")
|
||||
(epoch 100)
|
||||
(eval "(list id-session-test-pass id-session-test-count)")
|
||||
(epoch 101)
|
||||
@@ -88,6 +90,8 @@ cat > "$TMPFILE" << 'EPOCHS'
|
||||
(eval "(list id-audit-test-pass id-audit-test-count)")
|
||||
(epoch 109)
|
||||
(eval "(list id-fed-test-pass id-fed-test-count)")
|
||||
(epoch 110)
|
||||
(eval "(list id-expiry-test-pass id-expiry-test-count)")
|
||||
EPOCHS
|
||||
|
||||
timeout 600 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"language": "identity",
|
||||
"total_pass": 130,
|
||||
"total": 130,
|
||||
"total_pass": 138,
|
||||
"total": 138,
|
||||
"suites": [
|
||||
{"name":"session","pass":11,"total":11,"status":"ok"},
|
||||
{"name":"token","pass":24,"total":24,"status":"ok"},
|
||||
@@ -12,6 +12,7 @@
|
||||
{"name":"membership","pass":17,"total":17,"status":"ok"},
|
||||
{"name":"cache","pass":9,"total":9,"status":"ok"},
|
||||
{"name":"audit","pass":11,"total":11,"status":"ok"},
|
||||
{"name":"federation","pass":12,"total":12,"status":"ok"}
|
||||
{"name":"federation","pass":12,"total":12,"status":"ok"},
|
||||
{"name":"expiry","pass":8,"total":8,"status":"ok"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# identity-on-sx Scoreboard
|
||||
|
||||
**Total: 130 / 130 tests passing**
|
||||
**Total: 138 / 138 tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
@@ -14,6 +14,7 @@
|
||||
| ✅ | cache | 9 | 9 |
|
||||
| ✅ | audit | 11 | 11 |
|
||||
| ✅ | federation | 12 | 12 |
|
||||
| ✅ | expiry | 8 | 8 |
|
||||
|
||||
|
||||
Generated by `lib/identity/conformance.sh`.
|
||||
|
||||
92
lib/identity/tests/expiry.sx
Normal file
92
lib/identity/tests/expiry.sx
Normal file
@@ -0,0 +1,92 @@
|
||||
;; identity/tests/expiry.sx — access-token expiry on a logical clock
|
||||
;; (RFC 6749 §4.2.2 expires_in). `advance` stands in for time passing;
|
||||
;; introspect returns inactive once the clock reaches a token's expiry.
|
||||
;; Refresh mints a fresh short-lived access token — the point of refresh.
|
||||
|
||||
(define id-expiry-test-count 0)
|
||||
(define id-expiry-test-pass 0)
|
||||
(define id-expiry-test-fails (list))
|
||||
|
||||
(define
|
||||
id-expiry-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(set! id-expiry-test-count (+ id-expiry-test-count 1))
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! id-expiry-test-pass (+ id-expiry-test-pass 1))
|
||||
(append! id-expiry-test-fails {:name name :expected expected :actual actual}))))
|
||||
|
||||
(define ide-ev erlang-eval-ast)
|
||||
(define idenm (fn (v) (get v :name)))
|
||||
|
||||
(identity-load-token!)
|
||||
|
||||
;; ── within TTL is active; past TTL is inactive ───────────────────
|
||||
|
||||
(id-expiry-test
|
||||
"a token within its TTL is active"
|
||||
(idenm
|
||||
(ide-ev
|
||||
"R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read, 100),\n identity_tokens:advance(R, 50),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end"))
|
||||
"active")
|
||||
|
||||
(id-expiry-test
|
||||
"a token at its TTL boundary is expired"
|
||||
(idenm
|
||||
(ide-ev
|
||||
"R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read, 100),\n identity_tokens:advance(R, 100),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end"))
|
||||
"inactive")
|
||||
|
||||
(id-expiry-test
|
||||
"a token just before its TTL is still active"
|
||||
(idenm
|
||||
(ide-ev
|
||||
"R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read, 100),\n identity_tokens:advance(R, 99),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end"))
|
||||
"active")
|
||||
|
||||
;; ── no TTL (infinity) never expires ──────────────────────────────
|
||||
|
||||
(id-expiry-test
|
||||
"a token issued without a TTL never expires"
|
||||
(idenm
|
||||
(ide-ev
|
||||
"R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read),\n identity_tokens:advance(R, 100000),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end"))
|
||||
"active")
|
||||
|
||||
;; ── refresh mints a fresh short-lived token ──────────────────────
|
||||
|
||||
(id-expiry-test
|
||||
"refresh renews access after the old token expired"
|
||||
(idenm
|
||||
(ide-ev
|
||||
"R = identity_tokens:start(),\n {ok, A, Rt} = identity_tokens:issue_grant(R, alice, web, read, 100),\n identity_tokens:advance(R, 100),\n inactive = case identity_tokens:introspect(R, A) of\n {active, _, _, _} -> active; {inactive} -> inactive end,\n {ok, A2, _R2} = identity_tokens:refresh(R, Rt),\n case identity_tokens:introspect(R, A2) of\n {active, _, _, _} -> renewed;\n {inactive} -> inactive\n end"))
|
||||
"renewed")
|
||||
|
||||
(id-expiry-test
|
||||
"the renewed token also expires after its own TTL"
|
||||
(idenm
|
||||
(ide-ev
|
||||
"R = identity_tokens:start(),\n {ok, _A, Rt} = identity_tokens:issue_grant(R, alice, web, read, 100),\n identity_tokens:advance(R, 100),\n {ok, A2, _R2} = identity_tokens:refresh(R, Rt),\n identity_tokens:advance(R, 100),\n case identity_tokens:introspect(R, A2) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end"))
|
||||
"inactive")
|
||||
|
||||
;; ── the logical clock ────────────────────────────────────────────
|
||||
|
||||
(id-expiry-test
|
||||
"the clock starts at zero and advances"
|
||||
(ide-ev
|
||||
"R = identity_tokens:start(),\n identity_tokens:advance(R, 7),\n identity_tokens:advance(R, 35),\n identity_tokens:now(R)")
|
||||
42)
|
||||
|
||||
;; ── expiry composes with revocation ──────────────────────────────
|
||||
|
||||
(id-expiry-test
|
||||
"an expired token is also inactive after revoke (no contradiction)"
|
||||
(idenm
|
||||
(ide-ev
|
||||
"R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read, 100),\n identity_tokens:advance(R, 200),\n identity_tokens:revoke(R, T),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end"))
|
||||
"inactive")
|
||||
|
||||
(define
|
||||
id-expiry-test-summary
|
||||
(str "expiry " id-expiry-test-pass "/" id-expiry-test-count))
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user