;; 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))