oauth.sx routes the PKCE check through pkce_ok: an S256 challenge carried as
{s256, Hash} compares crypto:hash(sha256, Verifier) =:= Hash; a bare
challenge stays plain (§4.1), so both methods coexist with no change to
existing flows (the bare path is the old =:= behaviour). Raw sha256 digests
are compared (base64url is wire encoding, omitted). New tests/pkce.sx (6,
incl. S256 through PAR). Verified pkce 6/6; substrate fix is in the
preceding commit. 239 total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
4.3 KiB
Plaintext
93 lines
4.3 KiB
Plaintext
;; 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))
|