Typed claims, header, and JWKS document decoding for statically compiled verifiers - #42
Merged
Conversation
refresh! and default_jwk_alg read RFC 7517 JWK members out of JSON dicts as
untyped Any values, which made every downstream call — base64url decoding,
JWK construction, keyset insertion — dynamically dispatched and
unresolvable under juliac --trim=safe (68 of the verify errors in a
trim-compiled server whose auth verifier is built from a JWK set). JWK
members are strings by RFC, so ::String asserts on each member lookup turn
the whole per-key path into statically resolvable calls; a malformed member
now lands in refresh!'s existing per-key skip handling as a TypeError.
default_algs is normalized once to Dict{String,String}. Accepts JWK dicts
of any AbstractDict type as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verifier stored its keyset as the abstract VerifierKeySource and its clock
as ::Function, which type-erases both: a verifier built over a static
in-memory keyset still made the whole remote-JWKS/OIDC refresh machinery
statically reachable, and every now() call dispatched dynamically — eight
verify errors in a juliac --trim server that never fetches remote keys.
Verifier{S,F} keeps construction and the public API unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refresh!(url) indexed the parsed JWKS document as Any, and claim_audiences branched on AbstractString/AbstractVector; parsed JSON yields concrete String / Vector, so narrowing to those keeps both paths statically dispatched (the abstract fallbacks remain for non-JSON callers). Malformed JWKS documents now raise a clear ArgumentError instead of a KeyError. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Downloads was a hard dependency used only by fetch_url, so every JWTs consumer linked libcurl and its timer callbacks — two verify errors in a juliac --trim server that never fetches remote keys. HTTP.jl (already present across the JuliaWeb/JuliaServices stack) replaces it; the `downloader` keyword is accepted and ignored for compatibility (use `fetcher` to customize retrieval). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Verifier gains a claims-type parameter mirroring OAuth's store
design: Verifier(...; claims=MyClaims) decodes the payload with
JSON.parse(payload, MyClaims), and every validation read goes through
one accessor seam (claimvalue/hasclaim: dict lookup or struct field),
so claim_string/claim_number/claim_audiences never see an Any.
VerifiedJWT{C} carries the typed claims and claimstype(verifier)
reports it; the default Dict{String,Any} path is unchanged. The
audience check captures the narrowed local rather than the
Union{Nothing,...} field, and Verifier(keys::Vector; ...) forwards
keywords explicitly so the call resolves to the keyset constructor
alone instead of unioning over every keyword method.
The JOSE header now decodes into the typed JWTHeaderClaims - which
gains the registered typ member - instead of Dict{String,Any}, through
the same JSON-1-with-fallback route jwt_header_string_claim already
uses. VerifiedJWT.header changes type accordingly, and the alg/kid
validation reads become typed field accesses.
Under juliac --trim=safe this removes the JWTs-owned payload-decode
errors and the header decodepart error in a statically compiled server;
the refresh!-reachability pair on static key sets is a separate API
question and untouched here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The URL-refresh path read fetched documents through an untyped
JSON.parse merged with the custom-fetcher escape hatch, so the document
was an AbstractDict, its "keys" member an AbstractVector, and the
keyword call into refresh!(keys::Vector, ...) unresolvable under
juliac --trim - the one remaining JWTs-owned verify error in a
statically compiled server (the fetch itself was never the problem:
fetch_url returns a concrete String on both of its branches).
Fetched string/byte documents now parse as JWKSDocument, whose keys are
JWKSKey - one optional String per RFC 7517/7518 member the refresh
reads; unknown members (x5c, key_ops, ...) are skipped by the typed
parse. A first cut typed the keys as Vector{Dict{String,Any}}; that
reintroduced the known-toxic Dict{String,Any} parse target (makedict's
Any values plus the make(::Type{Any}) error-display machinery, +40
verify errors) and was replaced by the member struct. refresh! and
default_jwk_alg read members through jwk_member/jwk_optional_member: a
::String-asserted dict lookup with the original KeyError/TypeError
behavior for the public Vector-of-dicts path, or a constant-folded
field access for JWKSKey. The custom-fetcher arm (already-parsed
objects) and the pre-JSON-1 fallback stay dynamic by design.
Also: the typed-verifier test struct commits its aud to one arm
(String, matching how issuers write a single audience) and signs a
single-audience payload for the typed decode.
With this, JWTs contributes zero verify errors to the workshop app's
--trim=safe build on registered StructUtils; the residual set is Base
display/SIGINT machinery plus ScopedValues HAMT (julia#62730).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes every decode on the verification path concretely typed, so a server compiled with
juliac --trim=safeverifies with zero JWTs-owned errors (JuliaCon 2026 workshop app; the residual set there is now Base toolchain machinery only). Five commits, each keeping the default dynamic path unchanged:Verifier{S,F}→ parametric on key source and clock (earlier commits): a verifier over a static keyset no longer reaches the remote-JWKS machinery statically, and remote key sets are fetched with HTTP.jl instead of Downloads (thedownloaderkeyword is accepted and ignored;fetcherremains the customization point) — dropping libcurl from every consumer.Application-declared claims type:
Verifier(...; claims=MyClaims)decodes payloads withJSON.parse(payload, MyClaims), and every validation read goes through one accessor seam (claimvalue/hasclaim: dict lookup or struct field), soclaim_string/claim_number/claim_audiencesnever see anAny.VerifiedJWT{C}carries the typed claims;claimstype(verifier)reports it; the default remainsDict{String,Any}. Mirrors the store-side design in JuliaServices/OAuth.jl#46.Typed JOSE header:
JWTHeaderClaimsgains the registeredtypmember andverifydecodes headers into it (with the pre-JSON-1 dict fallback the code already used forjwt_header_string_claim).VerifiedJWT.headeris now aJWTHeaderClaims— a public field type change.Typed JWKS documents: fetched key-set documents parse as
JWKSDocument{keys::Vector{JWKSKey}}, whereJWKSKeyis one optionalStringper RFC 7517/7518 member the refresh reads (unknown members likex5care skipped).refresh!/default_jwk_algread members through a two-method seam — the publicVector-of-dicts path keeps its exactKeyError/TypeErrorbehavior; the typed path constant-folds to field accesses. The custom-fetcherarm and pre-JSON-1 fallback stay dynamic by design. Note for reviewers: a first cut typed the keys asVector{Dict{String,Any}}, which made things far worse under trim (Dict{String,Any}as a typed-parse target dragsmake(::Type{Any})and its error-display machinery into the graph) — the member struct is the shape that verifies.Suite green throughout, including the package's own trim harness; a typed-verifier test covers the declared-claims path end to end.
🤖 Generated with Claude Code
Review follow-up
Verifier(MyClaims, ...)API for static claim types. NoValAPI is used.Validation: Julia 1.10 and current suites pass. The current suite has 2,704 checks. The typed verification trim workload builds and runs with zero verifier errors. A clean local coverage run reports 100% coverage for changed source lines.
Co-authored by Codex