;; identity/tests/pkce.sx — PKCE S256 (RFC 7636 §4.2). The challenge is ;; sha256(verifier) (compared as raw digests — base64url is wire encoding, ;; omitted); the verifier is presented at exchange and re-hashed. An S256 ;; challenge is carried as a tagged {s256, Hash}; bare challenges remain ;; `plain` (RFC 7636 §4.1), so both methods coexist. Requires the fixed ;; erlang binary substrate (binary =:= + crypto:hash on binary literals). (define id-pkce-test-count 0) (define id-pkce-test-pass 0) (define id-pkce-test-fails (list)) (define id-pkce-test (fn (name actual expected) (set! id-pkce-test-count (+ id-pkce-test-count 1)) (if (= actual expected) (set! id-pkce-test-pass (+ id-pkce-test-pass 1)) (append! id-pkce-test-fails {:name name :expected expected :actual actual})))) (define idp-ev erlang-eval-ast) (define idpnm (fn (v) (get v :name))) (identity-load-oauth!) ;; Shared S256 prelude: verifier V, challenge = sha256(V), consented code. (define idp-s256 "V = <<\"verifier-abc-123\">>,\n Ch = crypto:hash(sha256, V),\n O = identity_oauth:start(),\n {consent_required, Rq} = identity_oauth:authorize(O, web, uri1, read, alice, {s256, Ch}),\n {code, Cd} = identity_oauth:consent(O, Rq, allow)") ;; ── S256 happy path ────────────────────────────────────────────── (id-pkce-test "S256: exchange with the matching verifier yields an active token" (idpnm (idp-ev (str idp-s256 ", {ok, A, _R} = identity_oauth:exchange(O, Cd, web, uri1, V),\n case identity_oauth:introspect(O, A) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end"))) "active") (id-pkce-test "S256: the token carries the authorized subject" (idpnm (idp-ev (str idp-s256 ", {ok, A, _R} = identity_oauth:exchange(O, Cd, web, uri1, V),\n case identity_oauth:introspect(O, A) of\n {active, Subject, _, _} -> Subject\n end"))) "alice") ;; ── S256 rejects a wrong verifier ──────────────────────────────── (id-pkce-test "S256: a wrong verifier is invalid_grant" (idpnm (idp-ev (str idp-s256 ", case identity_oauth:exchange(O, Cd, web, uri1, <<\"wrong-verifier\">>) of\n {ok, _, _} -> ok;\n {error, W} -> W\n end"))) "invalid_grant") (id-pkce-test "S256: the bare challenge hash is not accepted as the verifier" (idpnm (idp-ev (str idp-s256 ", case identity_oauth:exchange(O, Cd, web, uri1, Ch) of\n {ok, _, _} -> ok;\n {error, W} -> W\n end"))) "invalid_grant") ;; ── plain PKCE still works (coexistence) ───────────────────────── (id-pkce-test "plain: a bare challenge still matches its verifier" (idpnm (idp-ev "O = identity_oauth:start(),\n {consent_required, Rq} = identity_oauth:authorize(O, web, uri1, read, alice, plainverif),\n {code, Cd} = identity_oauth:consent(O, Rq, allow),\n {ok, A, _R} = identity_oauth:exchange(O, Cd, web, uri1, plainverif),\n case identity_oauth:introspect(O, A) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "active") ;; ── S256 threads through PAR ────────────────────────────────────── (id-pkce-test "S256 works through a pushed authorization request" (idpnm (idp-ev "V = <<\"par-verifier\">>,\n Ch = crypto:hash(sha256, V),\n O = identity_oauth:start(),\n {ok, Ru} = identity_oauth:push_authorization_request(O, web, uri1, read, alice, {s256, Ch}),\n {consent_required, Rq} = identity_oauth:authorize_pushed(O, Ru),\n {code, Cd} = identity_oauth:consent(O, Rq, allow),\n {ok, A, _R} = identity_oauth:exchange(O, Cd, web, uri1, V),\n case identity_oauth:introspect(O, A) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "active") (define id-pkce-test-summary (str "pkce " id-pkce-test-pass "/" id-pkce-test-count))