Skip to content

Refactor JWT into a Hypervel-native auth package#405

Merged
binaryfire merged 17 commits into
0.4from
feature/jwt-native-parity-refactor
Jun 28, 2026
Merged

Refactor JWT into a Hypervel-native auth package#405
binaryfire merged 17 commits into
0.4from
feature/jwt-native-parity-refactor

Conversation

@binaryfire

@binaryfire binaryfire commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Refactors the JWT package into a complete Hypervel-native authentication implementation while keeping the existing array payload model and adapting the internals for long-lived Swoole workers. This brings in the missing features from tymondesigns/jwt-auth / php-open-source-saver/jwt-auth, and fixes some security holes.

The package now includes centralized claim construction, optional JWTSubject support, issuer / issued-at / not-before validation, subject locking, hasListeners() guarded auth events, coroutine-scoped guard state, token parser/extractor support, secret and certificate generation commands, expanded configuration, and full documentation.

See plan for more details: docs/plans/2026-06-28-0709-jwt-hypervel-native-parity-refactor.md

Details

  • Adds an array-based ClaimFactory for standard claims, custom model claims, provider subject locking, issuer stamping, and refresh claim handling.
  • Adds optional JWTSubject support without requiring normal Authenticatable models to implement a JWT-specific contract.
  • Updates JwtGuard so token, payload, user, TTL, custom-claim, and last-attempted state are isolated through coroutine context.
  • Corrects attempt, once, onceUsingId, refresh, invalidation, and one-shot custom-claim behavior.
  • Adds hasListeners-guarded auth events so event objects are only built and dispatched when listeners exist.
  • Adds request token extraction from bearer headers, input, and optional cookies through a stateless parser chain.
  • Adds JWT middleware for authenticating requests and refreshing tokens in responses.
  • Adds jwt:secret and jwt:generate-certs commands, including HMAC, RSA, EC, passphrase, overwrite, and missing-env-file behavior.
  • Hardens Lcobucci provider behavior around missing signing material, configured keys, claim validation, and refresh-time validation.
  • Renames the typoed NotBeforeCliam class to NotBeforeClaim and verifies stale references are gone.
  • Adds config and docs for issuer validation, blacklist behavior, parser configuration, refresh settings, TTL settings, subject locking, and key management.
  • Keeps application JWT env vars out of the components root .env.example; only the Testbench app skeleton receives application-style JWT env examples.

Coroutine Safety And Performance

  • Guard mutable state is coroutine-scoped, not stored on worker-lifetime singletons.
  • Parser and middleware classes remain stateless.
  • Event dispatch follows the existing Hypervel hasListeners guard pattern.
  • Static caching is limited to immutable subject model class hashes and is reset by the test subscriber.
  • The hot path remains array-based and avoids restoring the upstream mutable claim / payload object graph.

Testing

  • composer fix
  • git diff --check
  • git -C ../hypervel diff --check
  • Targeted JWT test runs during implementation, including config, validation, guard, parser, middleware, provider, and command tests.

Summary by CodeRabbit

  • New Features

    • Added JWT authentication support with new token issuance, refresh, logout, and payload helpers.
    • Added console commands to generate JWT secrets and signing key pairs.
    • Added configurable token parsing from authorization headers, query input, and cookies.
  • Bug Fixes

    • Improved refresh behavior, token lifetime handling, and subject validation.
    • Fixed blacklist and invalidation edge cases so disabled blacklist mode no longer errors.
    • Corrected issuer and “not before” validation handling.
  • Documentation

    • Added and updated JWT setup and usage docs, including config and authentication guidance.

Document the researched JWT refactor plan before the implementation commits.

The plan records why the package should keep Hypervel's array-based, coroutine-safe architecture instead of restoring the upstream mutable object graph. It also captures the signed-off decisions for claim construction, parsing, middleware, console commands, validation, guard behavior, provider wiring, docs, and tests so the implementation has a durable reference.
Add the contracts needed for the Hypervel-native JWT surface.

This introduces optional JWTSubject support for model-controlled subject identifiers and custom claims, TemporalValidation so refresh can intentionally skip time-window checks, TokenExtractor for the parser chain, and a SecretMissingException for fail-fast signing configuration errors. The existing manager and validation contracts are widened to expose the new encode/decode/refresh behavior without bringing back the upstream mutable object model.
Modernize JWT claim validation around the native array payload model.

Expired and issued-at validation now participate in the TemporalValidation contract so refresh can bypass only time-window checks while still enforcing structural and issuer checks. Add issuer validation, replace the misspelled NotBeforeCliam class and test with NotBeforeClaim, and cover the corrected validation behavior with dedicated tests.
Add an array-based ClaimFactory that builds issue and refresh claims without restoring upstream claim objects.

The factory owns subject identifiers, model locking hashes, issuer stamping, standard temporal claims, custom claims, and refresh claim persistence. Static model-hash caching is limited to immutable class metadata and is reset through AfterEachTestSubscriber so tests do not leak worker-lifetime state.
Add Hypervel-native token extractors for authorization headers, cookies, and request input.

The parser chain stays stateless and singleton-safe: extractors read from the current request only when invoked and do not store token state on worker-lifetime services. Parser tests cover extractor order, bearer parsing, cookie parsing, input parsing, and missing-token behavior.
Add JWT middleware for authenticating requests and refreshing tokens in responses.

The middleware delegates token parsing and guard behavior to the existing JWT services, keeps no mutable request state of its own, and only catches expected authentication/token failures. Tests cover successful authentication, token renewal, refresh responses, missing tokens, and invalid-token failure paths.
Add the jwt:secret console command for generating and writing symmetric JWT secrets.

The command supports show, force, confirmation, and non-interactive skip flows. Tests isolate .env mutation with useEnvironmentPath() and ParallelTesting::tempDir(), matching the existing key generation precedent so environment writes do not leak across the worker's Testbench skeleton clone.
Add jwt:generate-certs for RSA and EC signing key generation.

The command writes certificate paths and algorithm settings into the configured environment file, supports encrypted and unencrypted private keys, refuses accidental overwrites without force, and fails before writing certificates when the environment file is missing. Tests cover RSA, EC, passphrase handling, overwrite protection, invalid algorithms, and isolated output cleanup.
Update JWTManager around the native claim factory and array payload model.

The manager now owns blacklist jti stamping for direct encode calls, supports decode-for-refresh validation rules, preserves persistent claims correctly during refresh, handles idempotent invalidation, and exposes the expanded facade surface. Tests cover encode, decode, refresh, blacklist, persistent-claim, and invalidation behavior so the token lifecycle stays consistent across guard and direct manager usage.
Tighten the Lcobucci provider around signing configuration and claim validation.

The provider now fails clearly when signing material is missing, handles symmetric and asymmetric algorithms through the configured keys, supports the new validation contract flow, and keeps decode behavior compatible with refresh-time temporal skipping. Tests cover missing secrets, configured secrets, key file usage, claim validation, and refresh validation paths.
Refactor JwtGuard around coroutine-scoped authentication state and the native claim factory.

The guard now keeps token, payload, user, ttl, custom claims, and last-attempted state isolated per coroutine, fixes attempt/once/onceUsingId semantics, resets one-shot custom claims after token creation, supports subject locking, and dispatches auth events only when listeners are registered. Tests cover the corrected API behavior, event dispatch guards, subject/provider matching, and coroutine isolation so auth state cannot bleed between concurrent requests.
Update JWT package wiring for the new native services and defaults.

The service provider now registers the claim factory, parser chain, console commands, manager, guard integration, middleware aliases, and package services explicitly. The config and environment examples expose the new token, issuer, blacklist, parser, and key options, while default validation enables required claims, expiration, issuer, issued-at, and not-before checks. Tests verify package discovery metadata, service bindings, config shape, parser defaults, and the corrected NotBeforeClaim class name.
Add full Boost documentation for Hypervel's JWT package and link it from the docs index and authentication guide.

The new JWT guide covers installation, guard setup, token issuing, refresh and invalidation, middleware, token extraction, custom claims, subject locking, issuer validation, blacklist behavior, key generation, asymmetric signing, configuration, and testing notes in the style of the existing Boost docs. The package README stays concise and points readers to the full framework documentation.
Remove JWT application configuration from the components root .env.example.

That file documents local integration-test environment variables for framework development, not application-level skeleton configuration. JWT defaults remain in the Testbench skeleton env example where application-style config belongs.
Document the JWT_DRIVER and JWT_ISSUER environment-backed config options in the Boost JWT guide.

JWT_DRIVER belongs with the signing provider configuration, and JWT_ISSUER belongs with issuer validation so every env-backed JWT option exposed by the package config is represented in the user-facing documentation.
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements a Hypervel-native JWT refactor: introduces ClaimFactory for array-based claim construction and subject-locking, a stateless Parser/TokenExtractor chain, a significantly refactored JwtGuard with coroutine-context state and event dispatch, refresh-aware JWTManager validation, IssuerClaim validation, jwt:secret/jwt:generate-certs console commands, updated provider wiring, and comprehensive tests and documentation.

Changes

JWT Hypervel-native parity refactor

Layer / File(s) Summary
Implementation plan and documentation
docs/plans/2026-06-28-0709-jwt-hypervel-native-parity-refactor.md, src/boost/docs/jwt.md, src/boost/docs/authentication.md, src/boost/docs/documentation.md, src/jwt/README.md
Adds a 2000-line implementation plan, a new JWT documentation page, authentication guidance updated to reference JWT, sidebar navigation entry, and package README differences section.
Contracts, exceptions, and validations
src/jwt/src/Contracts/JWTSubject.php, src/jwt/src/Contracts/TokenExtractor.php, src/jwt/src/Contracts/TemporalValidation.php, src/jwt/src/Contracts/ManagerContract.php, src/jwt/src/Contracts/ValidationContract.php, src/jwt/src/Exceptions/SecretMissingException.php, src/jwt/src/Validations/IssuerClaim.php, src/jwt/src/Validations/ExpiredClaim.php, src/jwt/src/Validations/IssuedAtClaim.php, src/jwt/src/Validations/NotBeforeClaim.php, src/support/src/Facades/JWT.php
Introduces JWTSubject, TokenExtractor, and TemporalValidation interfaces; adds SecretMissingException; expands ManagerContract::refresh() and adds hasBlacklistEnabled(); adds IssuerClaim validator; fixes NotBeforeClaim class spelling; marks ExpiredClaim/IssuedAtClaim as TemporalValidation.
Parser chain and JWT config
src/jwt/src/Http/Parser/AuthHeaders.php, src/jwt/src/Http/Parser/Cookie.php, src/jwt/src/Http/Parser/InputSource.php, src/jwt/src/Http/Parser/Parser.php, src/jwt/config/jwt.php, src/testbench/hypervel/.env.example, src/jwt/composer.json
Adds stateless AuthHeaders, Cookie, InputSource, and Parser token extractors; updates jwt.php with issuer, refresh_iat, lock_subject, parser chain config, integer-cast leeway, and enabled validators; adds hypervel/config and hypervel/console dependencies.
ClaimFactory and JWTManager refresh flow
src/jwt/src/ClaimFactory.php, src/jwt/src/JWTManager.php, src/jwt/src/Blacklist.php, src/jwt/src/Providers/Lcobucci.php
Introduces ClaimFactory for initial and refresh claim construction, subject-lock prv hashing, reserved-claim enforcement, and static hash cache; injects it into JWTManager; rewrites refresh() with temporal-validation skipping and decodeForRefresh(); conditionally injects jti; fixes Blacklist TTL to use ceil(); updates Lcobucci for file:// keys, audience arrays, withValidationConstraints(), and SecretMissingException.
JwtGuard coroutine state and events
src/jwt/src/JwtGuard.php
Injects ClaimFactory and Parser; rewires attempt(), login(), user(), refresh(), logout(), and invalidate() around token-keyed coroutine context; adds payload(), userOrFail(), getUserId()/id(), tokenById(), byId(), attempting(), getDispatcher()/setDispatcher(), and getLastAttempted(); extends event dispatch for Attempting/Validated/Failed/Login/Logout/Authenticated.
Console commands and provider wiring
src/jwt/src/Console/JwtSecretCommand.php, src/jwt/src/Console/JwtGenerateCertsCommand.php, src/jwt/src/JWTServiceProvider.php, src/auth/src/AuthManager.php, src/auth/src/AuthServiceProvider.php, src/auth/src/GuardHelpers.php, src/auth/src/SessionGuard.php
Adds jwt:secret (HMAC secret) and jwt:generate-certs (RSA/EC key pair) console commands; updates JWTServiceProvider to wire ClaimFactory, stateless Parser singleton, per-guard TTL, dispatcher injection, tagged-cache tag-support validation, and console command registration; fixes AuthManager to use make() and tightens AuthServiceProvider dispatcher rebinding logic.
JwtGuard, event, subject-lock, and coroutine tests
tests/JWT/JwtGuardTest.php, tests/JWT/JwtGuardEventTest.php, tests/JWT/JwtGuardSubjectLockTest.php, tests/JWT/JwtGuardCoroutineSafetyTest.php
Updates JwtGuardTest for real requests, expanded payload/cache/refresh/logout semantics, and helper APIs; adds event dispatch, subject-lock prv validation, and parallel coroutine isolation tests.
Manager, factory, parser, config, provider, and command tests
tests/JWT/ClaimFactoryTest.php, tests/JWT/JWTManagerTest.php, tests/JWT/Http/ParserTest.php, tests/JWT/JWTConfigTest.php, tests/JWT/JWTServiceProviderTest.php, tests/JWT/BlacklistTest.php, tests/JWT/Validations/IssuerClaimTest.php, tests/JWT/Validations/NotBeforeClaimTest.php, tests/JWT/Providers/LcobucciTest.php, tests/JWT/Console/JwtSecretCommandTest.php, tests/JWT/Console/JwtGenerateCertsCommandTest.php, tests/AfterEachTestSubscriber.php
Adds ClaimFactory, parser chain, IssuerClaim, NotBeforeClaim, JWTManager refresh, JWTServiceProvider TTL/blacklist/storage, Blacklist, Lcobucci, and console command tests; adds ClaimFactory::flushState() to global test teardown.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant JwtGuard
  participant Parser
  participant ClaimFactory
  participant JWTManager
  participant Dispatcher

  Client->>JwtGuard: attempt(credentials, login=true)
  JwtGuard->>Dispatcher: dispatch Attempting
  JwtGuard->>JwtGuard: retrieveByCredentials + validateCredentials
  alt valid credentials
    JwtGuard->>Dispatcher: dispatch Validated
    JwtGuard->>ClaimFactory: make(user, provider, ttl, customClaims)
    ClaimFactory-->>JwtGuard: claims array with sub/prv/iat/exp
    JwtGuard->>JWTManager: encode(claims)
    JWTManager-->>JwtGuard: JWT string
    JwtGuard->>Dispatcher: dispatch Authenticated + Login
    JwtGuard-->>Client: JWT string
  else invalid
    JwtGuard->>Dispatcher: dispatch Failed
    JwtGuard-->>Client: false
  end

  Client->>JwtGuard: refresh(forceForever, resetClaims)
  JwtGuard->>JWTManager: refresh(token, forceForever, resetClaims, customClaims, ttl)
  JWTManager->>JWTManager: decodeForRefresh — skip TemporalValidation validators
  JWTManager->>ClaimFactory: refresh(oldPayload, ttl, refreshIssuedAt, resetClaims, persistentClaims)
  ClaimFactory-->>JWTManager: refreshed claims
  JWTManager-->>JwtGuard: new JWT string
  JwtGuard-->>Client: new JWT string
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • hypervel/components#404: Changes per-token coroutine cache keys in JwtGuard from md5 to hash('xxh128', ...), which directly overlaps with this PR's token-keyed coroutine context refactor in JwtGuard.

Poem

🐇 Hop hop, the tokens flow,
Through parser chains and claims they go,
No coroutine shall bleed its state,
Each sub and prv locked by fate.
The factory mints, the guard protects,
And blacklists block with ceil-correct! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: refactoring JWT into a Hypervel-native authentication package.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/jwt-native-parity-refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR completes the JWT package with a large set of previously-missing features while adapting the implementation for Hypervel's long-lived Swoole workers. All per-request mutable guard state (token, user, payload, TTL, custom claims) is moved into CoroutineContext, ClaimFactory centralises claim construction for both issuance and refresh, and the Parser chain extracts tokens statefully per-call so no request data leaks across coroutines.

  • New ClaimFactory handles subject identification, optional JWTSubject custom claims, prv model-hash locking, issuer stamping, and managed refresh claim reconstruction, with static model-hash caching that is correctly reset by AfterEachTestSubscriber.
  • JwtGuard rewrite stores all request-scoped state in CoroutineContext keyed by guard name and token hash, fires auth events only when listeners are registered, and correctly orders user/token/payload cleanup in logout() and refresh() finally blocks.
  • Hardened Lcobucci provider throws typed exceptions for missing signing material; JwtGenerateCertsCommand now aborts on file-write failure before touching .env (addressing prior review comments); and the Blacklist precision fix uses ceil() to avoid premature entry expiry.

Confidence Score: 5/5

The new coroutine-scoped guard and claim-factory logic is well-structured and the ordering of context operations is correct throughout; the two comments are upgrade-path notes rather than runtime defects.

The core authentication flow (issue, decode, refresh, invalidate, logout) is implemented correctly with proper cleanup ordering. The two findings are both deployment-time concerns for applications upgrading from the previous JWT version rather than defects in the new code itself. No logic errors, race conditions, or security holes were found in the changed paths.

src/jwt/src/ClaimFactory.php and src/jwt/config/jwt.php warrant attention for any team upgrading an existing deployment that has live JWT tokens, as the subject-lock default and the newly-enabled expiry validations together can silently invalidate those tokens on upgrade.

Important Files Changed

Filename Overview
src/jwt/src/JwtGuard.php Major guard rewrite with coroutine-safe state via CoroutineContext, token-keyed user caching, guarded event dispatch, and full lifecycle management (login, logout, refresh, invalidate). Logic is sound and consistently ordered.
src/jwt/src/ClaimFactory.php New centralized claim builder for issuance and refresh. Well-structured, but lock_subject defaulting to true means tokens without prv (issued before this update) silently fail subjectMatchesProvider against Eloquent providers.
src/jwt/src/JWTManager.php Refactored manager delegates claim construction to ClaimFactory, correctly handles refresh window, blacklist, and TemporalValidation skip during refresh. jti generation for blacklist and forceForever handling are correct.
src/jwt/src/JWTServiceProvider.php Wires ClaimFactory, Parser chain, BlacklistContract, and guard extension correctly. Cache tag support check before enabling blacklist storage is a good defensive guard.
src/jwt/config/jwt.php Adds lock_subject (default true), issuer, refresh_iat, token/parser, and leeway cast. ExpiredClaim/IssuedAtClaim/NotBeforeClaim now enabled by default — previously commented out, which is a behavioral shift for upgrading deployments.
src/jwt/src/Console/JwtGenerateCertsCommand.php Addresses prior review comments: file write failures now throw before updating .env, and EC filenames use curve name rather than misleading bit count.
src/jwt/src/Providers/Lcobucci.php Hardened against missing signing material with explicit SecretMissingException on HMAC and JWTException on asymmetric keys. Configuration rebuild on key change is correctly ordered (signer before config).
src/jwt/src/Blacklist.php Precision fix: ceil() wrap on diffInMinutes() ensures fractional-minute differences round up, preventing premature blacklist entry expiry.
src/jwt/src/Http/Parser/Parser.php Stateless chain parser; request is passed per-call so coroutine requests cannot leak across singleton parsers.
src/jwt/src/Validations/IssuerClaim.php New issuer validator safely no-ops when jwt.issuer is unset, so adding it to the default validations list is non-breaking when no issuer is configured.

Reviews (3): Last reviewed commit: "refactor(jwt): remove sliding refresh mi..." | Re-trigger Greptile

Comment thread src/jwt/src/Console/JwtGenerateCertsCommand.php Outdated
Comment thread src/jwt/src/Console/JwtGenerateCertsCommand.php Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (3)
src/jwt/src/ClaimFactory.php (1)

158-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark flushState() as boot-or-tests-only.

This public static mutator clears worker-lifetime cached state, so its docblock should explicitly warn callers not to use it in normal request flow. As per coding guidelines, "Add boot-only, tests-only, or boot-or-tests-only warning docblock text to public mutators that change worker-lifetime state when the method is intended only for boot-time configuration or tests."

♻️ Proposed docblock update
     /**
      * Flush all static state.
+     *
+     * Warning: boot-or-tests-only. This mutates worker-lifetime cached state.
      */
     public static function flushState(): void
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jwt/src/ClaimFactory.php` around lines 158 - 163, Update the
ClaimFactory::flushState() docblock to clearly mark it as boot-or-tests-only,
since this public static mutator clears worker-lifetime cached state and should
not be used during normal request flow. Add the required warning text directly
above flushState() so callers are guided to use it only during boot-time
configuration or tests.

Source: Coding guidelines

src/jwt/src/JwtGuard.php (2)

92-99: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use the request from RequestContext directly.

Line 94 verifies coroutine request state, but Line 98 resolves request through the container, which depends on that binding being fresh/scoped. Passing RequestContext::get() removes the worker-lifetime binding ambiguity.

Suggested change
-        return $this->parser->parseToken($this->app->make('request'));
+        return $this->parser->parseToken(RequestContext::get());

As per coding guidelines, “audit make() calls for freshness versus auto-singleton behavior.” <coding_guidelines>

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jwt/src/JwtGuard.php` around lines 92 - 99, In JwtGuard::parseToken, the
request is being resolved from the container even after RequestContext::has()
confirms coroutine state, which can introduce stale worker-scoped bindings.
Update parseToken to use the request instance from RequestContext::get()
directly and pass that into parser->parseToken(), keeping the RequestContext
check as the guard and removing the app->make('request') dependency.

Source: Coding guidelines


439-460: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add boot-only warnings to worker-lifetime mutators.

attempting() registers listeners on the dispatcher and setDispatcher() swaps the guard dispatcher; both mutate worker-lifetime state and should say they are boot/tests-only.

Suggested docblock updates
     /**
      * Register an authentication attempt event listener.
+     *
+     * Boot-only. Listener registrations persist on the worker-lifetime dispatcher.
      */
     public function attempting(callable $callback): void
@@
     /**
      * Set the event dispatcher instance.
+     *
+     * Boot or tests only. The dispatcher is stored on the worker-lifetime guard.
      */
     public function setDispatcher(Dispatcher $events): void

As per coding guidelines, “Add boot-only, tests-only, or boot-or-tests-only warning docblock text to public mutators that change worker-lifetime state.” <coding_guidelines>

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jwt/src/JwtGuard.php` around lines 439 - 460, Update the public mutators
in JwtGuard—especially attempting() and setDispatcher()—to include the required
boot-only / tests-only warning docblock text. These methods register event
listeners and replace the dispatcher, so their PHPDoc should clearly mark them
as boot/tests-only worker-lifetime state changes per the coding guidelines. Keep
the existing method behavior unchanged and adjust the docblocks near
attempting(), getDispatcher(), and setDispatcher() as needed to match the
project’s warning wording.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jwt/config/jwt.php`:
- Around line 269-274: Make input-based JWT parsing opt-in by removing
Hypervel\JWT\Http\Parser\InputSource from the default parser chain in jwt config
while keeping Hypervel\JWT\Http\Parser\AuthHeaders as the secure default. Update
the jwt.php parser configuration so only header-based parsing is enabled by
default, and provide a clear way for apps to add InputSource explicitly when
they want to accept token input. Also review any related parser registration
logic in InputSource to ensure no other default path enables request-input
tokens implicitly.

In `@src/jwt/src/Console/JwtGenerateCertsCommand.php`:
- Around line 96-108: The key generation flow in
JwtGenerateCertsCommand::generate should not update the environment file until
both key writes succeed and the private key is secured. Check the return value
of file_put_contents for both the private key and public key, throw a
RuntimeException if either write fails, and explicitly set restrictive
permissions on the private key after writing it instead of relying on the
process umask. Only call Env::writeVariables once the key files are successfully
created and protected.
- Around line 39-76: The EC branch in JwtGenerateCertsCommand currently allows
mismatched ES algorithm and curve settings, so align the chosen `--sha` with the
curve used when building the OpenSSL options. Update the logic around the `match
($algorithm)` and the EC `curve_name` assignment to either validate and reject
invalid `--sha`/`--curve` combinations or automatically map `ES256`, `ES384`,
and `ES512` to their required curves in `JwtGenerateCertsCommand`.

In `@src/jwt/src/Http/Parser/AuthHeaders.php`:
- Around line 25-33: The auth header parsing in AuthHeaders::parseBearerToken is
too permissive because it uses strripos to find any occurrence of Bearer
anywhere in the header. Update the matching logic to only accept a
comma-delimited header segment that actually starts with the Bearer scheme, then
extract the token from that exact segment and keep the existing trimming/null
behavior.

In `@src/jwt/src/JwtGuard.php`:
- Around line 432-435: forgetUser() currently only clears the token-specific
user context, so a previously cached default user can still reappear after
setToken() or logout(). Update JwtGuard::forgetUser() to also clear the default
user cache key used by getUserContextKey()/user.default, and make sure the
cleanup covers both the active token context and the fallback default context.
- Around line 268-273: The JwtGuard::claims method currently allows reserved
identity claims like sub and prv to be merged into the pending claims context,
which can override the subject/provider-lock values used by ClaimFactory::make()
for the next token. Add a guard in JwtGuard::claims (or centrally in
ClaimFactory::make) to reject or strip reserved claims before storing them in
CoroutineContext, while still allowing non-reserved custom claims to merge
normally.

In `@src/jwt/src/JWTManager.php`:
- Around line 147-151: In JWTManager::refresh, the replacement token is being
invalidated too early, before the new token is successfully created. Update the
flow so encode($claims) happens first, then call invalidate($token,
$forceForever) only after encoding succeeds, and keep the blacklistEnabled check
around that invalidation step. This ensures the old token is only blacklisted if
a replacement token is actually available.
- Around line 138-145: Reject reserved JWT/subject-lock claims before refresh by
validating the custom claim set passed through JWTManager::refresh() into
ClaimFactory::refresh(). Ensure claims like exp, nbf, iss, and jti cannot be
reintroduced via customClaims after managed claims are stripped, ideally by
adding a central guard in ClaimFactory and/or filtering before the refresh call.
Use the existing claim-handling flow in JWTManager and ClaimFactory as the place
to enforce this consistently.

In `@src/jwt/src/Validations/NotBeforeClaim.php`:
- Line 11: The `NotBeforeClaim` marker is causing
`JWTManager::decodeForRefresh()` to skip `nbf`, which lets `refresh()` accept
tokens before they are active. Update the refresh path so `nbf` is still
validated during refresh, either by removing `TemporalValidation` from
`NotBeforeClaim` or by special-casing `JWTManager::decodeForRefresh()` to keep
`NotBeforeClaim` enabled. Also verify `ClaimFactory::refresh()` preserves the
original `nbf` instead of restamping it to now.

---

Nitpick comments:
In `@src/jwt/src/ClaimFactory.php`:
- Around line 158-163: Update the ClaimFactory::flushState() docblock to clearly
mark it as boot-or-tests-only, since this public static mutator clears
worker-lifetime cached state and should not be used during normal request flow.
Add the required warning text directly above flushState() so callers are guided
to use it only during boot-time configuration or tests.

In `@src/jwt/src/JwtGuard.php`:
- Around line 92-99: In JwtGuard::parseToken, the request is being resolved from
the container even after RequestContext::has() confirms coroutine state, which
can introduce stale worker-scoped bindings. Update parseToken to use the request
instance from RequestContext::get() directly and pass that into
parser->parseToken(), keeping the RequestContext check as the guard and removing
the app->make('request') dependency.
- Around line 439-460: Update the public mutators in JwtGuard—especially
attempting() and setDispatcher()—to include the required boot-only / tests-only
warning docblock text. These methods register event listeners and replace the
dispatcher, so their PHPDoc should clearly mark them as boot/tests-only
worker-lifetime state changes per the coding guidelines. Keep the existing
method behavior unchanged and adjust the docblocks near attempting(),
getDispatcher(), and setDispatcher() as needed to match the project’s warning
wording.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ae75aad8-d487-4d36-afd7-5fd1494a1909

📥 Commits

Reviewing files that changed from the base of the PR and between ffdf13c and 6b523f4.

📒 Files selected for processing (49)
  • docs/plans/2026-06-28-0709-jwt-hypervel-native-parity-refactor.md
  • src/boost/docs/authentication.md
  • src/boost/docs/documentation.md
  • src/boost/docs/jwt.md
  • src/jwt/README.md
  • src/jwt/composer.json
  • src/jwt/config/jwt.php
  • src/jwt/src/ClaimFactory.php
  • src/jwt/src/Console/JwtGenerateCertsCommand.php
  • src/jwt/src/Console/JwtSecretCommand.php
  • src/jwt/src/Contracts/JWTSubject.php
  • src/jwt/src/Contracts/ManagerContract.php
  • src/jwt/src/Contracts/TemporalValidation.php
  • src/jwt/src/Contracts/TokenExtractor.php
  • src/jwt/src/Contracts/ValidationContract.php
  • src/jwt/src/Exceptions/SecretMissingException.php
  • src/jwt/src/Http/Middleware/AuthenticateAndRenew.php
  • src/jwt/src/Http/Middleware/RefreshToken.php
  • src/jwt/src/Http/Parser/AuthHeaders.php
  • src/jwt/src/Http/Parser/Cookie.php
  • src/jwt/src/Http/Parser/InputSource.php
  • src/jwt/src/Http/Parser/Parser.php
  • src/jwt/src/JWTManager.php
  • src/jwt/src/JWTServiceProvider.php
  • src/jwt/src/JwtGuard.php
  • src/jwt/src/Providers/Lcobucci.php
  • src/jwt/src/Validations/ExpiredClaim.php
  • src/jwt/src/Validations/IssuedAtClaim.php
  • src/jwt/src/Validations/IssuerClaim.php
  • src/jwt/src/Validations/NotBeforeClaim.php
  • src/support/src/Facades/JWT.php
  • src/testbench/hypervel/.env.example
  • tests/AfterEachTestSubscriber.php
  • tests/JWT/ClaimFactoryTest.php
  • tests/JWT/Console/JwtGenerateCertsCommandTest.php
  • tests/JWT/Console/JwtSecretCommandTest.php
  • tests/JWT/Http/Middleware/AuthenticateAndRenewTest.php
  • tests/JWT/Http/Middleware/RefreshTokenTest.php
  • tests/JWT/Http/ParserTest.php
  • tests/JWT/JWTConfigTest.php
  • tests/JWT/JWTManagerTest.php
  • tests/JWT/JWTServiceProviderTest.php
  • tests/JWT/JwtGuardCoroutineSafetyTest.php
  • tests/JWT/JwtGuardEventTest.php
  • tests/JWT/JwtGuardSubjectLockTest.php
  • tests/JWT/JwtGuardTest.php
  • tests/JWT/Providers/LcobucciTest.php
  • tests/JWT/Validations/IssuerClaimTest.php
  • tests/JWT/Validations/NotBeforeClaimTest.php

Comment thread src/jwt/config/jwt.php
Comment thread src/jwt/src/Console/JwtGenerateCertsCommand.php
Comment thread src/jwt/src/Console/JwtGenerateCertsCommand.php
Comment thread src/jwt/src/Http/Parser/AuthHeaders.php Outdated
Comment thread src/jwt/src/JwtGuard.php
Comment thread src/jwt/src/JwtGuard.php
Comment thread src/jwt/src/JWTManager.php
Comment thread src/jwt/src/JWTManager.php Outdated
Comment thread src/jwt/src/Validations/NotBeforeClaim.php Outdated
Harden JWT certificate generation by validating supported SHA variants, enforcing EC curve/SHA compatibility, checking key writes, and securing generated private keys before writing environment values.

Protect package-managed JWT claims from user-provided custom claims, keep refresh from invalidating the old token until the replacement has been signed, and ensure refresh still enforces future not-before claims.

Tighten token parsing by defaulting the parser chain to authorization headers only and parsing comma-separated Authorization segments by scheme instead of matching stray Bearer substrings.

Clear default guard user context during JWT logout/forgetUser flows so default cached users cannot resurface after token override cleanup.

Document worker-lifetime guard mutators across JWT and session guards, add the shared provider mutator warning, and replace nearby container array access with explicit make() calls.

Adds regression coverage for certificate variants and failures, reserved claim rejection, refresh invalidation ordering, not-before refresh validation, parser behavior, config defaults, and guard context cleanup.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/JWT/JWTConfigTest.php (1)

152-158: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Flush Env before asserting parser defaults.

This test is reading an env-backed config file, but unlike the surrounding config tests it skips Env::flushRepository(). That makes the default-parser assertion order-dependent if another test has already populated the env repository.

Suggested fix
     public function testDefaultParserOnlyIncludesAuthorizationHeaders(): void
     {
+        Env::flushRepository();
+
         $config = require dirname(__DIR__, 2) . '/src/jwt/config/jwt.php';
 
         $this->assertSame([AuthHeaders::class], $config['parser']);
         $this->assertNotContains(InputSource::class, $config['parser']);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/JWT/JWTConfigTest.php` around lines 152 - 158, The default parser
config test is missing the same env cleanup used by the other config tests, so
its assertions can become order-dependent. Update
testDefaultParserOnlyIncludesAuthorizationHeaders in JWTConfigTest to flush the
env repository with Env::flushRepository() before loading
src/jwt/config/jwt.php, matching the surrounding config test setup and keeping
the parser defaults assertion isolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/JWT/JWTConfigTest.php`:
- Around line 152-158: The default parser config test is missing the same env
cleanup used by the other config tests, so its assertions can become
order-dependent. Update testDefaultParserOnlyIncludesAuthorizationHeaders in
JWTConfigTest to flush the env repository with Env::flushRepository() before
loading src/jwt/config/jwt.php, matching the surrounding config test setup and
keeping the parser defaults assertion isolated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d760cc51-06ae-493b-822f-e46a9a436a65

📥 Commits

Reviewing files that changed from the base of the PR and between 6b523f4 and dac24fb.

📒 Files selected for processing (19)
  • src/auth/src/AuthManager.php
  • src/auth/src/AuthServiceProvider.php
  • src/auth/src/GuardHelpers.php
  • src/auth/src/SessionGuard.php
  • src/boost/docs/jwt.md
  • src/jwt/config/jwt.php
  • src/jwt/src/ClaimFactory.php
  • src/jwt/src/Console/JwtGenerateCertsCommand.php
  • src/jwt/src/Http/Parser/AuthHeaders.php
  • src/jwt/src/JWTManager.php
  • src/jwt/src/JWTServiceProvider.php
  • src/jwt/src/JwtGuard.php
  • src/jwt/src/Validations/NotBeforeClaim.php
  • tests/JWT/ClaimFactoryTest.php
  • tests/JWT/Console/JwtGenerateCertsCommandTest.php
  • tests/JWT/Http/ParserTest.php
  • tests/JWT/JWTConfigTest.php
  • tests/JWT/JWTManagerTest.php
  • tests/JWT/JwtGuardTest.php
💤 Files with no reviewable changes (1)
  • src/jwt/config/jwt.php
✅ Files skipped from review due to trivial changes (2)
  • src/auth/src/GuardHelpers.php
  • src/auth/src/SessionGuard.php
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/boost/docs/jwt.md
  • tests/JWT/Http/ParserTest.php
  • src/jwt/src/JWTManager.php
  • src/jwt/src/Console/JwtGenerateCertsCommand.php
  • src/jwt/src/JWTServiceProvider.php
  • tests/JWT/ClaimFactoryTest.php
  • tests/JWT/JWTManagerTest.php
  • tests/JWT/JwtGuardTest.php
  • src/jwt/src/JwtGuard.php

Remove the JWT sliding refresh middleware aliases and document the explicit refresh endpoint pattern instead. Refreshing tokens on ordinary request middleware turns normal traffic into token rotation traffic, adds blacklist writes, and creates avoidable concurrent-request races.

Make blacklist storage resolution lazy so applications with blacklist disabled do not resolve cache-backed blacklist storage. Add a fail-fast taggable-store check only for the default TaggedCache storage while allowing custom storage implementations to define their own requirements.

Update the Lcobucci provider to use the current immutable withValidationConstraints API and round blacklist TTLs up so fractional minute differences cannot expire blacklist entries early.

Add regression coverage for disabled blacklist lazy resolution, tagged-cache capability checks, custom storage bypass behavior, alias removal, and blacklist TTL rounding. Update the JWT docs and plan addendum to capture the final Tymon follow-up decisions.
@binaryfire

Copy link
Copy Markdown
Collaborator Author

Added a follow-up commit for the Tymon JWT review findings: d06c59688.

Summary of the changes:

  • Removed the sliding refresh middleware aliases (jwt.refresh / jwt.renew) and their tests.
  • Documented the explicit refresh endpoint pattern instead, including why the refresh route should not be protected by auth:api.
  • Made blacklist storage resolution lazy so applications with jwt.blacklist_enabled=false do not resolve cache-backed blacklist storage.
  • Added a fail-fast taggable-store check only when the default TaggedCache blacklist storage is used and blacklist support is enabled.
  • Kept custom blacklist storage providers free to define their own storage requirements.
  • Switched the Lcobucci provider to the current immutable withValidationConstraints() API.
  • Rounded blacklist TTLs up so fractional minute differences cannot expire blacklist entries early.
  • Added regression coverage for alias removal, lazy blacklist resolution, taggable-store requirements, custom storage bypass behavior, and TTL rounding.

Reasoning:

  • Sliding refresh middleware turns normal request traffic into token rotation traffic, creates avoidable blacklist writes, and can introduce concurrent-request token replacement races. An explicit refresh endpoint is simpler, clearer, and better suited to Hypervel.
  • Disabled-blacklist applications should not pay setup cost or hit cache-driver requirements for a feature they are not using.
  • The taggable-cache requirement is now enforced at the right boundary: only for the default tagged blacklist storage, only when blacklist support is enabled.
  • The Lcobucci and TTL changes remove deprecated API usage and prevent early blacklist expiry without adding hot-path overhead.

Validation:

  • ./vendor/bin/phpunit --no-progress tests/JWT passed.
  • composer fix passed after rerun: cs fixer clean, phpstan clean, parallel test suite green.
  • Claude reviewed the follow-up implementation and signed off.

@binaryfire binaryfire merged commit 5195cff into 0.4 Jun 28, 2026
35 checks passed
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.

1 participant