Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions docs/upstream-sync/sync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,17 @@ aimeos/laravel-nestedset:

PHP-Open-Source-Saver/jwt-auth:
branch: main
checked_through: null
last_reviewed_pr: null
sync_date: null
checked_through: 1bdb72de5e60fcb02264d7aba4bd4608939e3798
last_reviewed_pr: 301
sync_date: '2026-10-07'
notes: Primary reference for the first-party hypervel/jwt package, which tracks useful capabilities, fixes and tested behavior rather than an exact API; tymondesigns/jwt-auth below is processed after it. Source mapping into src/jwt/src - JWT, JWTAuth, JWTGuard and Providers/Auth to JwtGuard; Manager to JwtManager and the Jwt facade; Factory, Claims, Payload and Support to ClaimFactory and array payloads; Validators/PayloadValidator and claim checks to Validations, which JwtManager runs; Validators/TokenValidator to the Lcobucci provider's parsing; Providers/JWT to Providers; Providers/Storage to Storage/CacheStorage; Http/Parser to Http/Parser and TokenExtractor; the service providers to JwtServiceProvider; Console and EnvHelperTrait to Console and Hypervel\Support\Env; config/config.php to config/jwt.php. Upstream middleware has no class. Tests map to tests/Jwt by owning class - Providers/JWT, TokenTest and Validators/TokenValidatorTest to Providers; Claims, FactoryTest, PayloadTest and Validators/PayloadValidatorTest to ClaimFactoryTest, JwtManagerTest and Validations; BlacklistTest and Providers/Storage to BlacklistTest and Storage/CacheStorageTest; ManagerTest to JwtManagerTest; JWTGuardTest, JWTAuthTest and Providers/Auth to the JwtGuard*Test files; Http/ParserTest and Middleware to Http/ParserTest, JwtServiceProviderTest and JwtGuardContextTest; LaravelServiceProviderTest to JwtServiceProviderTest; DefaultConfigValuesTest to JwtConfigTest; Console/EnvHelperTraitTest to tests/Support/EnvFileTest and Console. Upstream docs map to src/docs/jwt.md. Adapt changes to array payloads, configuration in place of runtime setters on shared services, and per-request guard state in CoroutineContext. src/jwt/README.md lists the deliberate differences and their equivalents; check them before applying an upstream change they affect, and keep it current.

tymondesigns/jwt-auth:
branch: 2.x
checked_through: 6c70930a92710d97e8e52b182fca2176097f33be
last_reviewed_pr: 2292
sync_date: '2026-10-07'
notes: Original package behind hypervel/jwt, processed after PHP-Open-Source-Saver/jwt-auth with the same source, test and docs mapping. It still receives independent fixes; compare each change with the fork's version and the Hypervel code before adapting it, without duplicating the fork's tests.

saloonphp/saloon:
branch: v4
Expand Down
213 changes: 159 additions & 54 deletions src/docs/jwt.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions src/docs/porting-from-laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [JSON:API Resources](#jsonapi-resources)
- [CSRF Protection](#csrf-protection)
- [Fortify](#fortify)
- [JWT Authentication](#jwt-authentication)
- [Scout](#scout)
- [Socialite](#socialite)
- [JSON Schema](#json-schema)
Expand Down Expand Up @@ -544,6 +545,15 @@ User models that use Fortify's `TwoFactorAuthenticatable` trait must also implem

Fortify ignores Laravel's `fortify.passwords` setting. Declare the password reset broker with the guard's `passwords` key in `config/auth.php` instead. See [password resets](/docs/{{version}}/fortify#password-resets). Laravel's deprecated `Laravel\Fortify\Rules\Password` rule is not available; use `Hypervel\Validation\Rules\Password`.

<a name="jwt-authentication"></a>
### JWT Authentication

Applications using `tymon/jwt-auth` or `php-open-source-saver/jwt-auth` can switch to `hypervel/jwt`. Publish its `config/jwt.php` file and copy your values into it instead of reusing the old file, since some options have been renamed or removed. Replace the `JWTAuth` and `JWTFactory` facades with guard methods such as `fromUser` and `payload`, which returns the claims as an array. Set options such as subject locking and the blacklist in configuration instead of calling setters at runtime. Replace the `jwt.auth` middleware with `auth:api`, remove `jwt.check` from routes that allow guests, and replace `jwt.refresh` and `jwt.renew` with a [refresh endpoint](/docs/{{version}}/jwt#refreshing-tokens).

Only the `Authorization` header is read by default. If clients send tokens in the query string, request body, or a cookie, add the matching parser to the `parser` option. See [token sources](/docs/{{version}}/jwt#token-sources).

Rotate `JWT_SECRET` or your key pair when you switch, so clients sign in again. Hypervel stores revocations under different cache keys, so tokens revoked by the old application would be accepted again if they still verified.

<a name="scout"></a>
### Scout

Expand Down
36 changes: 27 additions & 9 deletions src/jwt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,32 @@ JWT for Hypervel

Documentation: https://hypervel.org/docs/jwt

## Differences From php-open-source-saver/jwt-auth
## Differences From jwt-auth

- Hypervel uses array payloads instead of upstream `Payload`, `Token`, and claim DTO objects.
- Hypervel keeps the `Jwt` facade mapped to the array-based `JwtManager`, but does not include upstream `JwtAuth`, `JwtFactory`, or `JwtProvider` facades.
- Cookie token parsing is available but not enabled by default.
- Upstream route-parameter and Lumen parser shortcuts are not included.
- Upstream sliding refresh middleware is not included; use an explicit refresh endpoint that calls `Auth::guard(...)->refresh()`.
- Namshi and Lumen integrations are not included.
- The `show_black_list_exception` option is not included; JWT exceptions fail normally.
Hypervel JWT takes behavior, fixes and tests from both jwt-auth repositories below, with its own design: one guard and one manager serve every request in a worker, so per-request state lives in coroutine context, payloads are plain arrays, and settings come from configuration rather than setters on shared services.

Ported from: https://github.com/PHP-Open-Source-Saver/jwt-auth
- Payloads are arrays instead of upstream `Payload`, `Token` and claim objects. Read claims with array access, such as `payload()['sub']`, in place of `getClaim()` and the `Payload` methods.
- The `JWTAuth`, `JWTFactory` and `JWTProvider` facades are not included. Use the guard's methods, such as `Auth::guard('api')->fromUser($user)` or `login($user)`, to issue tokens for users. The `Jwt` facade's `encode()` signs the given payload without the claims the guard adds, except for a `jti` when the blacklist is enabled and the payload has none. `decode()` verifies a token and returns its claims. `Jwt::blacklist()` returns the blacklist and `Jwt::driver()` the JWT provider.
- Any `Authenticatable` user can receive tokens. Implement `JwtSubject`, upstream's `JWTSubject`, only to change the `sub` claim or add custom claims.
- The guard does not forward unknown methods to an upstream `JWT` object. In place of `JWTAuth::check()`, which reports whether the token is valid, use the guard's `check()` for an authenticated user or `Jwt::decode()` to validate a token.
- The guard always reads the current request, so it has no `setRequest()`; use `setToken()` to authenticate a specific token. Runtime setters such as `lockSubject()`, `setBlacklistEnabled()`, `setPersistentClaims()` and `setRefreshIat()` are not included because the guard and manager are shared by every request; set these options in `config/jwt.php`.
- Without a token, the guard's `refresh()` returns `null` and `payload()` returns an empty array, while upstream throws.
- The guard's `id()` loads the user like Laravel's guards and the original repository, while the PHP-Open-Source-Saver fork returns the token subject without loading the user. Use `getUserId()` for that.
- The JWT provider is chosen with the `driver` option and custom drivers are registered with `Jwt::extend()`, following Laravel's driver managers, instead of upstream's `providers.jwt` class setting.
- Custom claims may set registered claims such as `exp`, `iss` and `jti`, but not `sub` or `prv`, which always come from the user and its provider. Customize the subject with `JwtSubject::getJwtIdentifier()`.
- Hypervel adds a default `iss` only when `jwt.issuer` is set, rather than using the request URL, and a default `jti` only when the blacklist is enabled.
- Only `iat` and `sub` are required claims by default, because `iss`, `exp` and `jti` are only added when an issuer, a TTL or the blacklist is configured. The default `ttl` is 120 minutes rather than 60.
- Subject locking hashes the user provider's model class. While it is enabled, a guard whose provider exposes its model rejects tokens without a matching `prv` claim, including tokens that have none, and disabling it turns the check off. Upstream hashes the user's class, accepts tokens without `prv`, and still checks a `prv` claim while locking is disabled.
- Only the `Authorization` header is read by default, while upstream also reads the query string, request input, route parameters and cookies. Add `InputSource` to `jwt.parser` to read the query string and request body, where the body wins when both contain a token, or `Cookie` to read the cookie named by `cookie_key_name`.
- Upstream's separate query-string, route-parameter and Lumen parsers are not included. `InputSource` reads query input, and other sources such as route parameters use a custom `TokenExtractor`. The parser chain is set in `jwt.parser` instead of with upstream's `setChain()` and `addParser()`, because one parser is shared by every request.
- The `decrypt_cookies` option is not included. Upstream decrypts the cookie itself, which can't read cookies written by the `EncryptCookies` middleware, so that middleware decrypts the token cookie instead.
- Upstream's middleware is not included. Use `auth:api` instead of `jwt.auth`, and call `Auth::guard('api')->user()` on routes that allow guests instead of using `jwt.check`. Instead of the sliding refresh middleware (`jwt.refresh` and `jwt.renew`), use an explicit refresh endpoint that calls `Auth::guard(...)->refresh()`.
- The Namshi provider is not included. Lcobucci supports the same HMAC, RSA and ECDSA algorithms, and `tymondesigns/jwt-auth` no longer ships Namshi.
- Lumen integrations are not included.
- Clearing the blacklist requires a cache store that supports tags. On other stores upstream flushes the whole cache, so Hypervel throws the cache's tag error instead and never removes other cache entries.
- The `show_black_list_exception` option is not included. Upstream's option skips the blacklist check when disabled, so revoked tokens are accepted. Guards already treat blacklisted tokens as unauthenticated, and the exception handler's `dontReport` method keeps JWT exceptions out of logs.

Ported from:

- https://github.com/PHP-Open-Source-Saver/jwt-auth
- https://github.com/tymondesigns/jwt-auth
3 changes: 2 additions & 1 deletion src/jwt/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"hypervel/http": "^0.4",
"hypervel/macroable": "^0.4",
"hypervel/support": "^0.4",
"symfony/console": "^8.1.2"
"symfony/console": "^8.1.2",
"vlucas/phpdotenv": "^5.6.1"
},
"autoload": {
"psr-4": {
Expand Down
66 changes: 34 additions & 32 deletions src/jwt/config/jwt.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
| JWT Driver
|--------------------------------------------------------------------------
|
| The driver you are using to encode, decode and sign your
| JWT token, all the drivers must implement:
| Hypervel\Jwt\Contracts\ProviderContract::class
| The driver used to sign, encode and decode tokens. The "lcobucci"
| driver is built in. Register other drivers with `Jwt::extend()`;
| they must implement Hypervel\Jwt\Contracts\ProviderContract.
|
*/

Expand Down Expand Up @@ -99,10 +99,10 @@
| Defaults to 2 hours.
|
| You can also set this to null, to yield a never expiring token.
| Some people may want this behaviour for e.g. a mobile app.
| Some people may want this behavior for e.g. a mobile app.
| This is not particularly recommended, so make sure you have appropriate
| systems in place to revoke the token if necessary.
| Notice: If you set this to null you should remove 'exp' element from 'required_claims' list.
| Notice: If you set this to null, 'exp' must not be listed in 'required_claims'.
|
*/

Expand Down Expand Up @@ -193,9 +193,10 @@
| Persistent Claims
|--------------------------------------------------------------------------
|
| Specify the claim keys to be persisted when refreshing a token.
| `sub` and `iat` will automatically be persisted, in
| addition to the these claims.
| Specify the claim keys to keep when a token is refreshed with reset
| claims. On every refresh, `sub` and `prv` are kept, `nbf`, `exp` and
| `jti` are rebuilt, and `iat` is kept unless `refresh_iat` is enabled.
| Without a reset, other claims are kept too.
|
| Note: If a claim does not exist then it will be ignored.
|
Expand All @@ -215,7 +216,8 @@
| Meaning that if you have any unavoidable slight clock skew on
| any of your servers then this will afford you some level of cushioning.
|
| This applies to the claims `iat`, `nbf` and `exp`.
| This applies to the claims `iat`, `nbf` and `exp`, and to the end of
| the refresh window.
|
| Specify in seconds - only if you know you need it.
|
Expand All @@ -233,7 +235,20 @@
|
*/

'blacklist_enabled' => (bool) env('JWT_BLACKLIST_ENABLED', false),
'blacklist_enabled' => (bool) env('JWT_BLACKLIST_ENABLED', true),
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

/*
|--------------------------------------------------------------------------
| Blacklist Cache Store
|--------------------------------------------------------------------------
|
| The cache store used by the default blacklist storage. Use a store that
| is shared by every server, such as Redis, so revocations apply on all
| of them. Set it to null to use the default cache store.
|
*/

'blacklist_store' => env('JWT_BLACKLIST_STORE'),

/*
|--------------------------------------------------------------------------
Expand Down Expand Up @@ -264,13 +279,15 @@
| Token Parser
|--------------------------------------------------------------------------
|
| Configure the request input key and ordered parser chain used to extract
| JWT tokens from incoming requests.
| Configure the request input key, cookie name and ordered parser chain
| used to extract JWT tokens from incoming requests.
|
*/

'token' => env('JWT_TOKEN', 'token'),

'cookie_key_name' => env('JWT_COOKIE_KEY_NAME', 'token'),

'parser' => [
\Hypervel\Jwt\Http\Parser\AuthHeaders::class,
],
Expand All @@ -280,11 +297,9 @@
| Blacklist Grace Period
| -------------------------------------------------------------------------
|
| When multiple concurrent requests are made with the same JWT,
| it is possible that some of them fail, due to token regeneration
| on every request.
|
| Set grace period in seconds to prevent parallel request failure.
| A revoked token remains usable for this many seconds, allowing
| concurrent requests that use it to finish. Tokens invalidated with
| `forceForever` are revoked immediately.
|
*/

Expand All @@ -301,29 +316,16 @@
*/

'providers' => [
/*
|--------------------------------------------------------------------------
| JWT Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to create and decode the tokens.
|
*/

'jwt' => Hypervel\Jwt\Providers\Lcobucci::class,

/*
|--------------------------------------------------------------------------
| Storage Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to store tokens in the blacklist.
| The default tagged-cache storage requires a taggable default cache
| store; with node-local stack tiers, blacklist visibility is bounded
| by the upper tier's TTL.
| The default cache storage uses the `blacklist_store` cache store.
|
*/

'storage' => Hypervel\Jwt\Storage\TaggedCache::class,
'storage' => Hypervel\Jwt\Storage\CacheStorage::class,
],
];
14 changes: 10 additions & 4 deletions src/jwt/src/Blacklist.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@

class Blacklist implements BlacklistContract
{
/**
* Create a new blacklist instance.
*/
public function __construct(
protected StorageContract $storage,
protected ?int $refreshTTL,
protected int $gracePeriod = 0,
protected ?int $refreshTTL = 20160,
protected int $leeway = 0,
protected string $key = 'jti'
) {
Expand Down Expand Up @@ -43,15 +46,15 @@ public function add(array $payload): bool
}

$expiresAt = $expiresAt->max(
$this->timestamp($issuedAt)->addMinutes($this->refreshTTL)
$this->timestamp($issuedAt)->addMinutes($this->refreshTTL)->addSeconds($this->leeway)
);
}

$expiresAt = $expiresAt->addMinute();
$now = Date::now();

// The unified boundary covers expiration acceptance, including leeway, and
// the refresh window, so terminal tokens need no cache I/O.
// The unified boundary covers expiration acceptance and the refresh window,
// each including leeway, so terminal tokens need no cache I/O.
if ($expiresAt <= $now) {
return true;
}
Expand Down Expand Up @@ -220,6 +223,9 @@ public function getRefreshTTL(): ?int
return $this->refreshTTL;
}

/**
* Create a date from a Unix timestamp.
*/
protected function timestamp(int $timestamp): CarbonInterface
{
return Date::createFromTimestamp($timestamp);
Expand Down
33 changes: 13 additions & 20 deletions src/jwt/src/ClaimFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
class ClaimFactory
{
/**
* Claims stamped by the factory itself when a token is refreshed.
* Claims that belong to a single token and are rebuilt when it is refreshed.
*/
protected const array MANAGED_REFRESH_CLAIMS = ['iat', 'nbf', 'exp', 'iss', 'jti'];
protected const array MANAGED_REFRESH_CLAIMS = ['iat', 'nbf', 'exp', 'jti'];

/**
* Claims owned by the guard, manager, or claim factory.
* Identity claims derived from the user and provider.
*/
protected const array RESERVED_CUSTOM_CLAIMS = ['sub', 'prv', ...self::MANAGED_REFRESH_CLAIMS];
protected const array RESERVED_CUSTOM_CLAIMS = ['sub', 'prv'];

protected static array $subjectModelHashes = [];

Expand Down Expand Up @@ -81,25 +81,18 @@ public function refresh(
array $persistentClaims,
array $customClaims = [],
): array {
$managed = array_flip(self::MANAGED_REFRESH_CLAIMS);
$persistent = array_diff_key(
array_intersect_key($payload, array_flip($persistentClaims)),
$managed,
);

$claims = $resetClaims
? $persistent
: array_diff_key($payload, $managed);
$carried = $resetClaims
? array_intersect_key($payload, array_flip($persistentClaims))
: $payload;

$this->rejectReservedCustomClaims($customClaims);

$claims = array_merge($claims, $persistent, $customClaims, [
'sub' => $payload['sub'],
]);

if (! $refreshIssuedAt) {
$claims['iat'] = $payload['iat'];
}
$claims = array_merge(
array_diff_key($carried, array_flip(self::MANAGED_REFRESH_CLAIMS)),
$refreshIssuedAt ? [] : ['iat' => $payload['iat']],
$customClaims,
['sub' => $payload['sub']],
);

if (array_key_exists('prv', $payload)) {
$claims['prv'] = $payload['prv'];
Expand Down
Loading
Loading