Skip to content

feat(conformance): sign JWTs per-case in the harness (closes #41)#54

Merged
milmazz merged 1 commit into
mainfrom
jwt-signing-harness-41
Jul 3, 2026
Merged

feat(conformance): sign JWTs per-case in the harness (closes #41)#54
milmazz merged 1 commit into
mainfrom
jwt-signing-harness-41

Conversation

@milmazz

@milmazz milmazz commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #41 by teaching the conformance harness to mint and sign a JWT from each case's request.jwt block, enabling the 12 pending auth cases (1460–1464, 1468–1474). Enabling them immediately exposed lib-side conformance bugs in the JWT error mapping, fixed in the same change — the first real end-to-end exercise of the verification path from #35.

Harness

  • Bier.HttpCase.perform/1 signs the case's payload with jose and sends it as Authorization: Bearer. The symbolic sign_with: hs256_test_secret resolves to PostgREST's testCfg secret via a map in the harness (same pattern as the asymmetric JWK in Bier.ConformanceServer), keeping the YAML declarative.
  • The payload is signed as-is: several cases deliberately carry invalid claim types (e.g. a string exp) that the server must reject, so the harness must not validate or coerce them.
  • Cases 1468–1473 configure jwt-aud: youraudience, which is mutually exclusive with the shared instance's jwt_aud: nil, so they are added to @variant_case_ids and run on dedicated instances. 1474 (jwt-aud: null) matches the shared config and stays put.
  • The :jwt arm of the pending_reason cond is gone.

Library fixes the cases exposed

Bier.JWT detected non-numeric exp/nbf/iat, wrong-typed aud, and audience mismatches, but collapsed them all into :jwt_invalid, rendered as PGRST301 / "JWSError JWSInvalidSignature". PostgREST v14.12 returns PGRST303 with specific messages. It also skipped aud type-checking entirely when no jwt-aud was configured, accepting object-valued aud claims with a 200.

  • Bier.JWT now returns distinct reasons: {:claim_not_number, claim}, :aud_not_string, :not_in_audience; aud is type-checked unconditionally while membership is still only enforced when jwt-aud is configured (empty-array aud remains "no audience").
  • Bier.Plugs.FallbackController maps each to 401 PGRST303 with PostgREST's exact wording (The JWT 'exp' claim must be a number, The JWT 'aud' claim must be a string or an array of strings, JWT not in audience).

Testing

🤖 Generated with Claude Code

Enable the 12 pending auth cases (1460-1464, 1468-1474) by minting and
signing a bearer token from each case's request.jwt block:

- Bier.HttpCase signs the payload as-is with jose (symbolic
  sign_with: hs256_test_secret -> the PostgREST testCfg secret), so
  deliberately-invalid claims reach the server untouched.
- Cases 1468-1473 set jwt-aud, conflicting with the shared instance's
  jwt_aud: nil, so they run on dedicated variant instances.
- Drop the :jwt pending arm from the conformance generator.

Running the cases exposed lib-side conformance bugs, fixed here:
Bier.JWT collapsed non-numeric exp/nbf/iat, wrong-typed aud, and
audience mismatch into :jwt_invalid (PGRST301 "JWSError
JWSInvalidSignature") and only type-checked aud when jwt-aud was
configured. It now returns distinct reasons ({:claim_not_number, claim},
:aud_not_string, :not_in_audience), type-checks aud unconditionally, and
FallbackController maps each to PGRST303 with PostgREST's exact message.

All 45 auth-area cases pass; full suite green except the pre-existing
PostGIS geojson cases (#15).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@milmazz
milmazz force-pushed the jwt-signing-harness-41 branch from cb02efb to 81eb0bb Compare July 3, 2026 18:47
@milmazz
milmazz merged commit 7049739 into main Jul 3, 2026
3 checks passed
@milmazz
milmazz deleted the jwt-signing-harness-41 branch July 3, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[conformance][harness] Sign JWTs per-case for auth cases (request.jwt) — 12 pending cases

1 participant