Files
rose-ash/lib/identity/tests/sso.sx
giles 56cf920041
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 43s
identity: silent SSO prompt=none fast-path — one session, many clients (10 tests)
oauth.sx now owns a session registry. establish creates a subject session;
silent_authorize (OIDC prompt=none §3.1.2.1) asks "does this subject have a
live session?" — if yes it mints a code skipping consent, bound to client +
redirect_uri + PKCE exactly like a consented code; if no it returns
login_required (a negative state, not a login redirect). One session serves
many clients; end_session closes the fast-path. New tests/sso.sx. 75/75.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 00:45:15 +00:00

116 lines
5.9 KiB
Plaintext

;; identity/tests/sso.sx — silent SSO (prompt=none, OIDC §3.1.2.1) as a
;; fast-path through the authorization-code machine. One subject session,
;; many client apps; no session → login_required (a negative state, not a
;; redirect). Silently-issued codes carry the same client/redirect/PKCE
;; binding as consented codes.
(define id-sso-test-count 0)
(define id-sso-test-pass 0)
(define id-sso-test-fails (list))
(define
id-sso-test
(fn
(name actual expected)
(set! id-sso-test-count (+ id-sso-test-count 1))
(if
(= actual expected)
(set! id-sso-test-pass (+ id-sso-test-pass 1))
(append! id-sso-test-fails {:name name :expected expected :actual actual}))))
(define ids-ev erlang-eval-ast)
(define idsnm (fn (v) (get v :name)))
(identity-load-token!)
(identity-load-session!)
(identity-load-registry!)
(identity-load-oauth!)
;; ── no session → login_required ──────────────────────────────────
(id-sso-test
"silent authorize without a session is login_required"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n case identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv) of\n {code, _} -> got_code;\n {error, Why} -> Why\n end"))
"login_required")
;; ── established session → silent code ────────────────────────────
(id-sso-test
"silent authorize for the same client returns a code"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, _Sid} = identity_oauth:establish(O, alice, web),\n case identity_oauth:silent_authorize(O, web, uri1, read, alice, vv) of\n {code, _} -> got_code;\n {error, Why} -> Why\n end"))
"got_code")
;; ── one session, many clients ────────────────────────────────────
(id-sso-test
"a different client gets a silent code off the same session"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, _Sid} = identity_oauth:establish(O, alice, web),\n case identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv) of\n {code, _} -> got_code;\n {error, Why} -> Why\n end"))
"got_code")
(id-sso-test
"many clients all silently authorize off one session"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, _Sid} = identity_oauth:establish(O, alice, web),\n {code, _C1} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n {code, _C2} = identity_oauth:silent_authorize(O, mobile, uri3, read, alice, vv),\n case identity_oauth:silent_authorize(O, billing, uri4, read, alice, vv) of\n {code, _} -> got_code;\n {error, Why} -> Why\n end"))
"got_code")
;; ── full SSO → token ─────────────────────────────────────────────
(id-sso-test
"silent code exchanges to a working token"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, _Sid} = identity_oauth:establish(O, alice, web),\n {code, C} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n {ok, A, _R} = identity_oauth:exchange(O, C, dashboard, uri2, vv),\n case identity_oauth:introspect(O, A) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end"))
"active")
(id-sso-test
"SSO token carries the subject"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, _Sid} = identity_oauth:establish(O, alice, web),\n {code, C} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n {ok, A, _R} = identity_oauth:exchange(O, C, dashboard, uri2, vv),\n case identity_oauth:introspect(O, A) of\n {active, Subject, _, _} -> Subject\n end"))
"alice")
;; ── silent codes keep the full binding ───────────────────────────
(id-sso-test
"silent code still enforces PKCE at exchange"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, _Sid} = identity_oauth:establish(O, alice, web),\n {code, C} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n case identity_oauth:exchange(O, C, dashboard, uri2, wrongverif) of\n {ok, _, _} -> ok;\n {error, Why} -> Why\n end"))
"invalid_grant")
(id-sso-test
"silent code still enforces client binding at exchange"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, _Sid} = identity_oauth:establish(O, alice, web),\n {code, C} = identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv),\n case identity_oauth:exchange(O, C, attacker, uri2, vv) of\n {ok, _, _} -> ok;\n {error, Why} -> Why\n end"))
"invalid_grant")
;; ── subject scoping: SSO is per subject ──────────────────────────
(id-sso-test
"another subject is still login_required"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, _Sid} = identity_oauth:establish(O, alice, web),\n case identity_oauth:silent_authorize(O, dashboard, uri2, read, bob, vv) of\n {code, _} -> got_code;\n {error, Why} -> Why\n end"))
"login_required")
;; ── ending the session closes the SSO fast-path ──────────────────
(id-sso-test
"after end_session, silent authorize is login_required"
(idsnm
(ids-ev
"O = identity_oauth:start(),\n {ok, Sid} = identity_oauth:establish(O, alice, web),\n identity_oauth:end_session(O, Sid),\n case identity_oauth:silent_authorize(O, dashboard, uri2, read, alice, vv) of\n {code, _} -> got_code;\n {error, Why} -> Why\n end"))
"login_required")
(define
id-sso-test-summary
(str "sso " id-sso-test-pass "/" id-sso-test-count))