;; identity/tests/oauth.sx — OAuth2 authorization-code flow (RFC 6749 ;; §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) (define id-oauth-test-fails (list)) (define id-oauth-test (fn (name actual expected) (set! id-oauth-test-count (+ id-oauth-test-count 1)) (if (= actual expected) (set! id-oauth-test-pass (+ id-oauth-test-pass 1)) (append! id-oauth-test-fails {:name name :expected expected :actual actual})))) (define ido-ev erlang-eval-ast) (define idonm (fn (v) (get v :name))) (identity-load-token!) (identity-load-oauth!) ;; Shared prelude: authorize + consent(allow) leaving Code bound. (define ido-granted "O = identity_oauth:start(),\n {consent_required, ReqId} =\n identity_oauth:authorize(O, webapp, uri1, read, alice, verif1),\n {code, Code} = identity_oauth:consent(O, ReqId, allow)") ;; ── full happy path ────────────────────────────────────────────── (id-oauth-test "authorize asks for consent" (idonm (ido-ev "O = identity_oauth:start(),\n case identity_oauth:authorize(O, webapp, uri1, read, alice, verif1) of\n {consent_required, _} -> consent_required;\n Other -> Other\n end")) "consent_required") (id-oauth-test "consent(allow) returns a code" (idonm (ido-ev (str ido-granted ", case Code of _ -> issued end"))) "issued") (id-oauth-test "exchanged access token introspects active" (idonm (ido-ev (str ido-granted ", {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 "exchanged token carries the authorized subject" (idonm (ido-ev (str ido-granted ", {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 "exchanged token carries the authorized scope" (idonm (ido-ev (str ido-granted ", {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 "denied consent yields access_denied" (idonm (ido-ev "O = identity_oauth:start(),\n {consent_required, ReqId} =\n identity_oauth:authorize(O, webapp, uri1, read, alice, verif1),\n case identity_oauth:consent(O, ReqId, deny) of\n {error, Why} -> Why;\n {code, _} -> issued\n end")) "access_denied") ;; ── single-use codes (§10.5) ───────────────────────────────────── (id-oauth-test "code cannot be exchanged twice" (idonm (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"))) "invalid_grant") ;; ── code binding to client + redirect_uri (§4.1.3) ─────────────── (id-oauth-test "exchange with wrong client is invalid_grant" (idonm (ido-ev (str ido-granted ", case identity_oauth:exchange(O, Code, attacker, uri1, verif1) of\n {ok, _, _} -> ok;\n {error, Why} -> Why\n end"))) "invalid_grant") (id-oauth-test "exchange with wrong redirect_uri is invalid_grant" (idonm (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"))) "invalid_grant") ;; ── PKCE verifier mismatch (RFC 7636) ──────────────────────────── (id-oauth-test "exchange with wrong PKCE verifier is invalid_grant" (idonm (ido-ev (str ido-granted ", case identity_oauth:exchange(O, Code, webapp, uri1, badverif) of\n {ok, _, _} -> ok;\n {error, Why} -> Why\n end"))) "invalid_grant") ;; ── unknown code / request ─────────────────────────────────────── (id-oauth-test "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")) "invalid_grant") (id-oauth-test "consent on an unknown request is unknown_request" (idonm (ido-ev "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") ;; ── revoke-then-use must fail (RFC 7009) ───────────────────────── (id-oauth-test "revoked exchanged token introspects inactive" (idonm (ido-ev (str ido-granted ", {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, _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 id-oauth-test-summary (str "oauth " id-oauth-test-pass "/" id-oauth-test-count))