identity: wire refresh into oauth + e2e flow tests (Phase 2 complete, +3 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 53s

exchange now issues an access+refresh pair (RFC 6749 §4.1.4/§5.1) via
token.sx issue_grant; added the refresh grant (§6) delegating to token
rotation. End-to-end: code-exchange → refresh → introspect (active),
refresh-token reuse rejected (invalid_grant), and revoke-then-refresh
blocked by grant cascade. oauth 17/17, 65/65.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 00:35:10 +00:00
parent baee67f561
commit 20ba152e36
5 changed files with 67 additions and 28 deletions

View File

@@ -1,8 +1,9 @@
;; identity/tests/oauth.sx — OAuth2 authorization-code flow (RFC 6749
;; §4.1) + PKCE (RFC 7636). Covers the full happy path and every
;; rejection: denied consent, single-use codes, client/redirect binding,
;; PKCE verifier mismatch, unknown code/request, and real revocation of
;; an exchanged token.
;; §4.1) + PKCE (RFC 7636) + refresh grant (§6). Covers the full happy
;; path end-to-end (code exchange → access+refresh → refresh rotation) and
;; every rejection: denied consent, single-use codes, client/redirect
;; binding, PKCE mismatch, unknown code/request, refresh-token reuse, and
;; revoke-then-use (which must fail).
(define id-oauth-test-count 0)
(define id-oauth-test-pass 0)
@@ -44,12 +45,12 @@
"issued")
(id-oauth-test
"exchanged token introspects active"
"exchanged access token introspects active"
(idonm
(ido-ev
(str
ido-granted
", {ok, Tok} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n case identity_oauth:introspect(O, Tok) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")))
", {ok, Tok, _R} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n case identity_oauth:introspect(O, Tok) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")))
"active")
(id-oauth-test
@@ -58,7 +59,7 @@
(ido-ev
(str
ido-granted
", {ok, Tok} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n case identity_oauth:introspect(O, Tok) of\n {active, Subject, _, _} -> Subject\n end")))
", {ok, Tok, _R} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n case identity_oauth:introspect(O, Tok) of\n {active, Subject, _, _} -> Subject\n end")))
"alice")
(id-oauth-test
@@ -67,9 +68,29 @@
(ido-ev
(str
ido-granted
", {ok, Tok} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n case identity_oauth:introspect(O, Tok) of\n {active, _, _, Scope} -> Scope\n end")))
", {ok, Tok, _R} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n case identity_oauth:introspect(O, Tok) of\n {active, _, _, Scope} -> Scope\n end")))
"read")
;; ── refresh grant (RFC 6749 §6) end-to-end ───────────────────────
(id-oauth-test
"refresh after exchange yields a working access token"
(idonm
(ido-ev
(str
ido-granted
", {ok, _A, R} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n {ok, A2, _R2} = identity_oauth:refresh(O, R),\n case identity_oauth:introspect(O, A2) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")))
"active")
(id-oauth-test
"reusing a rotated refresh token is invalid_grant"
(idonm
(ido-ev
(str
ido-granted
", {ok, _A, R} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n {ok, _A2, _R2} = identity_oauth:refresh(O, R),\n case identity_oauth:refresh(O, R) of\n {ok, _, _} -> rotated;\n {error, Why} -> Why\n end")))
"invalid_grant")
;; ── consent denied (§4.1.2.1) ────────────────────────────────────
(id-oauth-test
@@ -87,7 +108,7 @@
(ido-ev
(str
ido-granted
", identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n case identity_oauth:exchange(O, Code, webapp, uri1, verif1) of\n {ok, _} -> replayed;\n {error, Why} -> Why\n end")))
", identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n case identity_oauth:exchange(O, Code, webapp, uri1, verif1) of\n {ok, _, _} -> replayed;\n {error, Why} -> Why\n end")))
"invalid_grant")
;; ── code binding to client + redirect_uri (§4.1.3) ───────────────
@@ -98,7 +119,7 @@
(ido-ev
(str
ido-granted
", case identity_oauth:exchange(O, Code, attacker, uri1, verif1) of\n {ok, _} -> ok;\n {error, Why} -> Why\n end")))
", case identity_oauth:exchange(O, Code, attacker, uri1, verif1) of\n {ok, _, _} -> ok;\n {error, Why} -> Why\n end")))
"invalid_grant")
(id-oauth-test
@@ -107,7 +128,7 @@
(ido-ev
(str
ido-granted
", case identity_oauth:exchange(O, Code, webapp, evil_uri, verif1) of\n {ok, _} -> ok;\n {error, Why} -> Why\n end")))
", case identity_oauth:exchange(O, Code, webapp, evil_uri, verif1) of\n {ok, _, _} -> ok;\n {error, Why} -> Why\n end")))
"invalid_grant")
;; ── PKCE verifier mismatch (RFC 7636) ────────────────────────────
@@ -118,7 +139,7 @@
(ido-ev
(str
ido-granted
", case identity_oauth:exchange(O, Code, webapp, uri1, badverif) of\n {ok, _} -> ok;\n {error, Why} -> Why\n end")))
", case identity_oauth:exchange(O, Code, webapp, uri1, badverif) of\n {ok, _, _} -> ok;\n {error, Why} -> Why\n end")))
"invalid_grant")
;; ── unknown code / request ───────────────────────────────────────
@@ -127,7 +148,7 @@
"exchanging an unknown code is invalid_grant"
(idonm
(ido-ev
"O = identity_oauth:start(),\n Bogus = make_ref(),\n case identity_oauth:exchange(O, Bogus, webapp, uri1, verif1) of\n {ok, _} -> ok;\n {error, Why} -> Why\n end"))
"O = identity_oauth:start(),\n Bogus = make_ref(),\n case identity_oauth:exchange(O, Bogus, webapp, uri1, verif1) of\n {ok, _, _} -> ok;\n {error, Why} -> Why\n end"))
"invalid_grant")
(id-oauth-test
@@ -137,7 +158,7 @@
"O = identity_oauth:start(),\n Bogus = make_ref(),\n case identity_oauth:consent(O, Bogus, allow) of\n {code, _} -> issued;\n {error, Why} -> Why\n end"))
"unknown_request")
;; ── revocation is real on an exchanged token (RFC 7009) ──────────
;; ── revoke-then-use must fail (RFC 7009) ─────────────────────────
(id-oauth-test
"revoked exchanged token introspects inactive"
@@ -145,16 +166,25 @@
(ido-ev
(str
ido-granted
", {ok, Tok} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n identity_oauth:revoke(O, Tok),\n case identity_oauth:introspect(O, Tok) of\n {active, _, _, _} -> still_valid;\n {inactive} -> inactive\n end")))
", {ok, Tok, _R} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n identity_oauth:revoke(O, Tok),\n case identity_oauth:introspect(O, Tok) of\n {active, _, _, _} -> still_valid;\n {inactive} -> inactive\n end")))
"inactive")
(id-oauth-test
"revoking the access token blocks a later refresh (cascade)"
(idonm
(ido-ev
(str
ido-granted
", {ok, A, R} = identity_oauth:exchange(O, Code, webapp, uri1, verif1),\n identity_oauth:revoke(O, A),\n case identity_oauth:refresh(O, R) of\n {ok, _, _} -> refreshed;\n {error, Why} -> Why\n end")))
"invalid_grant")
;; ── independence: two concurrent authorizations don't collide ────
(id-oauth-test
"two authorizations issue independent grants"
(idonm
(ido-ev
"O = identity_oauth:start(),\n {consent_required, R1} =\n identity_oauth:authorize(O, webapp, uri1, read, alice, va),\n {consent_required, R2} =\n identity_oauth:authorize(O, cli, uri2, write, bob, vb),\n {code, C1} = identity_oauth:consent(O, R1, allow),\n {code, C2} = identity_oauth:consent(O, R2, allow),\n {ok, _T1} = identity_oauth:exchange(O, C1, webapp, uri1, va),\n {ok, T2} = identity_oauth:exchange(O, C2, cli, uri2, vb),\n case identity_oauth:introspect(O, T2) of\n {active, Subject, _, _} -> Subject\n end"))
"O = identity_oauth:start(),\n {consent_required, R1} =\n identity_oauth:authorize(O, webapp, uri1, read, alice, va),\n {consent_required, R2} =\n identity_oauth:authorize(O, cli, uri2, write, bob, vb),\n {code, C1} = identity_oauth:consent(O, R1, allow),\n {code, C2} = identity_oauth:consent(O, R2, allow),\n {ok, _A1, _RR1} = identity_oauth:exchange(O, C1, webapp, uri1, va),\n {ok, A2, _RR2} = identity_oauth:exchange(O, C2, cli, uri2, vb),\n case identity_oauth:introspect(O, A2) of\n {active, Subject, _, _} -> Subject\n end"))
"bob")
(define