Skip to content

feat(auth): JWT verifiers, claim/header enums, and docs split#20

Merged
ChiragAgg5k merged 8 commits into
mainfrom
feat/auth-jwt-verifiers
Jun 22, 2026
Merged

feat(auth): JWT verifiers, claim/header enums, and docs split#20
ChiragAgg5k merged 8 commits into
mainfrom
feat/auth-jwt-verifiers

Conversation

@ChiragAgg5k

@ChiragAgg5k ChiragAgg5k commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Adds a generic, dependency-free JWS verifier layer to utopia-php/auth (the mirror of the existing issuers), hardens it, modernizes the package, and splits the docs.

Verifiers (new)

  • Verifier (base) — splits the compact JWS, base64url/JSON-decodes, enforces the alg-confusion guard, and validates standard claims. Concrete Verifiers\Asymmetric (RS256, public key) and Verifiers\Symmetric (HS256, shared secret) delegate only the signature check. Verifiers\VerificationException is thrown on any failure; verify() otherwise returns the decoded claims. Verifiers\Asymmetric::getKeyId() derives the kid the same way the issuer does.
  • Immutable & coroutine-safe: configuration (issuer, audience, type, allowExpired, leeway) is passed via readonly constructor params with named args — no fluent setters that a shared instance could have flipped mid-verification:
    new Asymmetric($publicKey, issuer: $iss, audience: $aud, type: 'at+jwt', leeway: 30)
  • Validation guarantees: exp is required and must be in the future; nbf/iat are always enforced when present; allowExpired: true relaxes only the expiry check (e.g. an OIDC id_token_hint). The typ header can be pinned to stop one token kind being accepted in place of another, and header/claims segments that decode to a JSON array (not an object) are rejected.
  • Stays zero-dependency (native openssl/hash).

Enums (new)

  • Enums\Claim (RFC 7519 registered claims + OAuth2/OIDC) and Enums\Header (RFC 7515 JOSE params), threaded through both the verifier and the issuers in place of scattered string literals.

Modernization

  • readonly + constructor property promotion for set-once key material across the issuers and verifiers; Proof defaults its hash via a new initializer.
  • Password now keeps its active hash aligned with its registry (a custom registry's algorithm is actually used, and removeHash's current-hash guard matches).
  • Bumps the php constraint to >= 8.1 (enums + string-keyed array spread).

Tooling

  • Enables phpstan (level 5) and rector for the package (matching sibling packages) and applies their fixes (package-wide declare(strict_types=1), final test classes, redundant-assertion cleanup, etc.).

Docs

  • Splits the oversized README into a lean overview plus a docs/ folder (hashing, proofs, store, jwt); removes the dead Travis build badge.

Testing

  • bin/monorepo check auth — pint + phpstan + rector all pass.
  • bin/monorepo test auth156 tests, 380 assertions passing, including verifier round-trips and negative paths (tampered signature/claims, wrong key, expired/allowExpired, missing exp, future nbf/iat, iss/aud/typ/alg mismatch, non-object segments, malformed).

Notes

  • Scope is the utopia-php/auth package only. A companion cloud-side refactor (consuming these verifiers, deduping the access-token issuer, dropping Ahc\Jwt) lives in a separate repo and depends on a tagged release of this package, so it ships separately.
  • The wider Hash/Proof/Store classes keep their existing fluent setters (out of scope for this PR).

Add a generic JWS verifier layer mirroring the existing Issuers tree:
Verifier (base) owns compact-JWS decode, the alg-confusion guard, and
standard exp/nbf/iat/iss/aud claim validation with clock-skew leeway;
Verifiers\Asymmetric (RS256) and Verifiers\Symmetric (HS256) delegate
only the signature check. Stays dependency-free (native openssl/hash).

Introduce Enums\Claim (RFC 7519 + OAuth2/OIDC claim names) and
Enums\Header (RFC 7515 JOSE params) and thread them through both the
verifier and the issuers, replacing scattered string literals. Enums and
string-keyed array spread require PHP 8.1, so bump the constraint to >=8.1.

Split the oversized README into a lean overview plus a docs/ folder
(hashing, proofs, store, jwt) and replace the dead Travis badge with the
monorepo GitHub Actions badge.
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a JWT verifier layer (Verifier base + Verifiers\Asymmetric/Verifiers\Symmetric subclasses) that mirrors the existing issuer tree, introduces Enums\Claim and Enums\Header backed enums to replace scattered string literals across both issuers and verifiers, fixes the hash-registry alignment in Password, and splits the README into a docs/ folder.

  • The verifier correctly guards against algorithm-confusion (alg check before signature), enforces exp presence by default, and keeps nbf/iat checks independent of allowExpired, with consistent clock-skew leeway across all time-based claims.
  • The Enums migration threads through both the new verifiers and the existing issuers with no behavioral regressions; 148 tests (382 assertions) cover the full round-trip and negative paths.
  • The PHP requirement bump to >=8.1 is appropriate given the use of backed enums and new expressions in constructor default values.

Confidence Score: 5/5

The new verifier layer is well-structured and the security-critical paths (alg guard, signature verification, claim enforcement) are all correct and covered by tests.

The algorithm-confusion guard, constant-time HMAC comparison, exp/nbf/iat leeway logic, and the immutable constructor design are all implemented correctly. The two findings are minor documentation and consistency nits that do not affect runtime behavior.

packages/auth/src/Auth/Verifiers/Asymmetric.php is missing declare(strict_types=1); packages/auth/src/Auth/Verifier.php has a misleading verify() docblock.

Important Files Changed

Filename Overview
packages/auth/src/Auth/Verifier.php New base verifier class; alg/type checks, claim validation, and leeway logic are correct, but the verify() docblock describes the execution order backwards (alg is checked before the signature, not after).
packages/auth/src/Auth/Verifiers/Asymmetric.php New RS256 verifier; logic and key handling are sound, but the file is missing declare(strict_types=1) unlike its sibling Symmetric.php and the parent Verifier.php.
packages/auth/src/Auth/Verifiers/Symmetric.php New HS256 verifier; uses hash_equals for constant-time comparison and correctly enforces the shared-secret requirement.
packages/auth/src/Auth/Enums/Claim.php New backed enum for RFC 7519 / OAuth2 / OIDC claim names; covers all claims used by the issuers and verifiers.
packages/auth/src/Auth/Enums/Header.php New backed enum for JOSE header parameters; complete and correct.
packages/auth/src/Auth/Issuers/Asymmetric/AccessToken.php Migrated to Claim/Header enums; audience validation loop no longer checks that each element is a string (the is_string guard was removed in this PR).
packages/auth/src/Auth/Proofs/Password.php Fixes hash-state alignment: the active hash is now taken from the registered registry (Argon2 if present, first entry otherwise) rather than always creating a new Argon2 instance.
packages/auth/tests/Auth/Verifiers/AsymmetricTest.php New test suite covering round-trips, tampered signatures, wrong keys, alg mismatch, exp/nbf/iat enforcement, type checks, and non-object claims.
packages/auth/tests/Auth/Verifiers/SymmetricTest.php New test suite for HS256 verifier covering happy path, wrong secret, expiry, audience mismatch, and leeway tolerance.

Reviews (4): Last reviewed commit: "(refactor): make the verifier immutable ..." | Re-trigger Greptile

Comment thread packages/auth/src/Auth/Proofs/Password.php
Comment thread packages/auth/src/Auth/Verifier.php Outdated
Comment thread packages/auth/src/Auth/Verifier.php Outdated
Comment thread packages/auth/src/Auth/Verifier.php
Comment thread packages/auth/src/Auth/Verifier.php Outdated
Comment thread packages/auth/src/Auth/Issuers/Asymmetric/AccessToken.php
Comment thread packages/auth/src/Auth/Verifier.php
Address review findings in the verifier and Password proof:
- Require "exp" and always enforce "nbf"/"iat": allowExpired() now relaxes
  only the expiry check, so a not-yet-valid or future-issued token is still
  rejected, and a token with no "exp" no longer verifies forever.
- Add setType() to pin the "typ" header (e.g. at+jwt), preventing one token
  kind from being accepted in place of another.
- Reject JWS header/claims segments that decode to a JSON array rather than
  an object.
- Password: keep the active hash aligned with the registry so a custom
  registry's algorithm is actually used and removeHash()'s current-hash guard
  matches.

Adds regression tests for each.
Replace the verifier's fluent setters (setIssuer/setAudience/setType/
allowExpired/setLeeway) with readonly constructor parameters passed via named
arguments. Mutable fluent setters on a shared instance are a coroutine footgun
— another coroutine can flip the expectations mid-verification — so the
configuration is now set once and held read-only.

Concrete verifiers take the key plus the optional expectations and forward
them to the base constructor; tests and docs use the named-argument form.
@ChiragAgg5k ChiragAgg5k merged commit 0795581 into main Jun 22, 2026
4 checks passed
@ChiragAgg5k ChiragAgg5k deleted the feat/auth-jwt-verifiers branch June 22, 2026 11:58
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.

2 participants