PHPackages                             sopheak/sp-jwt-auth - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. sopheak/sp-jwt-auth

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

sopheak/sp-jwt-auth
===================

First-party JWT access and rotating refresh token authentication for Laravel apps.

0.1.16(1mo ago)12↓50%[2 PRs](https://github.com/sopheaksem9999/sp-jwt-auth/pulls)MITPHPPHP ^8.3|^8.4|^8.5CI passing

Since Jun 11Pushed 1w agoCompare

[ Source](https://github.com/sopheaksem9999/sp-jwt-auth)[ Packagist](https://packagist.org/packages/sopheak/sp-jwt-auth)[ RSS](/packages/sopheak-sp-jwt-auth/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (10)Dependencies (9)Versions (13)Used By (0)

SP JWT Auth
===========

[](#sp-jwt-auth)

[![Latest Stable Version](https://camo.githubusercontent.com/6ddce2d8e6002da69ade8c2d8f4f59db0d5032e4cdc1202ecce4fbebf4e0591c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f736f706865616b2f73702d6a77742d617574682e737667)](https://packagist.org/packages/sopheak/sp-jwt-auth)[![Total Downloads](https://camo.githubusercontent.com/4bdbae3024c56909c5c34a8de5fb6400616b95015b13cad4630b05cde01c6ea5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f736f706865616b2f73702d6a77742d617574682e737667)](https://packagist.org/packages/sopheak/sp-jwt-auth)[![PHP Version](https://camo.githubusercontent.com/409b6dcdb50efb8218cb23f526342537c334daa4173cef32ce71e22c4176abb2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f736f706865616b2f73702d6a77742d617574682f7068702e737667)](composer.json)[![License](https://camo.githubusercontent.com/d00ac81b33380225e6d4fb855234dd4f4760d62db57f7887770d4a230fb7841a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f736f706865616b2f73702d6a77742d617574682e737667)](LICENSE)[![Security](https://github.com/sopheak/sp-jwt-auth/actions/workflows/security.yml/badge.svg)](https://github.com/sopheak/sp-jwt-auth/actions/workflows/security.yml)

`sopheak/sp-jwt-auth` is a modular Laravel authentication package for first-party JWT APIs, rotating opaque refresh tokens, account security workflows, API keys, external identity links, and optional OAuth server mode.

The package owns authentication infrastructure. Your application still owns password login, registration, user creation, tenants, roles, UI, response shape, delivery templates, and business authorization policy.

Features
--------

[](#features)

ModuleWhat it providesDefaultCore JWT`sp-jwt` guard, signed JWT access tokens, persisted `jti`, opaque rotating refresh tokens, scopes, claims, revocation, key rotation, JWKS, events, hooksEnabledAccount SecurityMFA challenge broker, hashed OTP codes, email verification tokens, password reset tokens, app-owned sender contractsDisabledAPI KeysScoped integration keys with public-id lookup, HMAC secret validation, rotation, revocation, IP restrictions, middlewareDisabledExternal IdentityNormalized Socialite/OIDC-style identity DTO, provider contract, external identity storageDisabledOAuth ServerSeparate `sp_oauth_*` storage, clients, consents, authorization-code + PKCE, refresh tokens, client credentials, revocation, introspection, resource middlewareDisabledRequirements
------------

[](#requirements)

- PHP `^8.3|^8.4|^8.5`
- Laravel `^12.0|^13.0`
- `firebase/php-jwt`
- RSA signing keys for the default `RS256` setup

Optional integrations are kept in Composer `suggest`:

- `laravel/socialite`
- `socialiteproviders/manager`
- `league/oauth2-client`
- `league/oauth2-server`

Stability
---------

[](#stability)

This package is pre-1.0. APIs, config keys, and optional module behavior may change before `v1.0.0`. Pin a tagged version in production and review the changelog before upgrading.

Installation
------------

[](#installation)

The package is public on Packagist: [sopheak/sp-jwt-auth](https://packagist.org/packages/sopheak/sp-jwt-auth).

Install it with Composer:

```
composer require sopheak/sp-jwt-auth
php artisan sp-jwt-auth:setup --keys
php artisan migrate
php artisan sp-jwt-auth:validate
```

The setup command publishes config and migrations, attempts to add the Laravel `api` guard, generates local PEM signing keys with `--keys`, and writes the related JWT key paths and refresh hash secret to `.env`. If your `config/auth.php` is custom, add the guard manually:

```
'guards' => [
    'api' => [
        'driver' => 'sp-jwt',
        'provider' => 'users',
    ],
],
```

Keep Laravel's normal `web` guard for Blade, Livewire, Inertia, and session pages.

For local path testing while developing the package:

```
composer config repositories.sp-jwt-auth '{"type":"path","url":"/absolute/path/to/sp-jwt-auth","options":{"versions":{"sopheak/sp-jwt-auth":"0.1.0"}}}'
composer require sopheak/sp-jwt-auth:^0.1
```

Configuration
-------------

[](#configuration)

Publish the config when needed:

```
php artisan vendor:publish --tag=sp-jwt-auth-config
```

Common environment keys:

```
SP_JWT_GUARD=api
SP_JWT_USER_PROVIDER=users
SP_JWT_ISSUER=https://app.example.com
SP_JWT_AUDIENCE=app-api
SP_JWT_ALGORITHM=RS256
SP_JWT_ACCESS_TTL_MINUTES=15
SP_JWT_REFRESH_TTL_DAYS=60
SP_JWT_REUSE_DETECTION=revoke_session
SP_JWT_ACTIVE_KID=2026-06-primary
SP_JWT_PRIVATE_KEY_PATH=storage/jwt-private-2026-06-primary.pem
SP_JWT_PUBLIC_KEY_PATH=storage/jwt-public-2026-06-primary.pem
SP_JWT_HASH_KEY_ID=default
SP_JWT_REFRESH_HASH_KEY=your-random-refresh-hash-secret
```

Optional modules have their own config sections:

- `mfa`
- `email_verification`
- `password_reset`
- `api_keys`
- `external_identities`
- `oauth_server`

Quick Start
-----------

[](#quick-start)

Create login and refresh endpoints in your Laravel app. Your app owns credential validation; the package owns token issuing, refresh rotation, and token response formatting.

```
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\ValidationException;
use Sopheak\JwtAuth\DTO\TokenContext;
use Sopheak\JwtAuth\Services\JwtTokenService;
use Sopheak\JwtAuth\Support\TokenResponse;

Route::post('/login', function (Request $request, JwtTokenService $jwt) {
    $credentials = $request->validate([
        'email' => ['required', 'email'],
        'password' => ['required', 'string'],
    ]);

    $user = User::query()->where('email', $credentials['email'])->first();

    if (! $user || ! Hash::check($credentials['password'], $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    $pair = $jwt->issueTokenPair(
        $user,
        TokenContext::make()->scopes(['profile.read']),
    );

    return TokenResponse::passportCompatible($pair);
});

Route::post('/refresh', function (Request $request, JwtTokenService $jwt) {
    $data = $request->validate([
        'refresh_token' => ['required', 'string'],
    ]);

    return TokenResponse::passportCompatible(
        $jwt->rotateRefreshToken($data['refresh_token']),
    );
});

Route::middleware('auth:api')->get('/me', function (Request $request) {
    return $request->user();
});
```

Call protected routes with the returned access token:

```
Authorization: Bearer
```

Core JWT Usage
--------------

[](#core-jwt-usage)

Your app validates credentials, resolves a user, builds a `TokenContext`, then asks the package to issue tokens.

```
use Sopheak\JwtAuth\DTO\TokenContext;
use Sopheak\JwtAuth\Services\JwtTokenService;
use Sopheak\JwtAuth\Support\TokenResponse;

$pair = app(JwtTokenService::class)->issueTokenPair(
    $user,
    TokenContext::make()
        ->companyId(42)
        ->companyIds([42, 84])
        ->scopes(['invoices.read', 'invoices.write'])
        ->impersonated(false),
);

return TokenResponse::passportCompatible($pair);
```

Read claims from the authenticated token:

```
$token = $request->user()?->token();

$companyId = $token?->claim('company_id');
$claims = $token?->claims ?? [];
```

For response fields owned by the app, pass extra data to the response helper or register a response extension:

```
return TokenResponse::passportCompatible($pair, [
    'company_id' => $pair->accessTokenRecord->companyId(),
]);

TokenResponse::extend(function (array $response, TokenPair $pair): array {
    $response['company_id'] = $pair->accessTokenRecord->companyId();
    $response['impersonated'] = $pair->accessTokenRecord->isImpersonated();

    return $response;
});
```

Protect routes with Laravel auth middleware:

```
Route::middleware(['auth:api'])->get('/me', MeController::class);

Route::middleware(['auth:api', 'sp.jwt.scope:invoices.read'])
    ->get('/invoices', InvoiceIndexController::class);
```

Add Passport-like helpers to user models:

```
use Sopheak\JwtAuth\Traits\HasJwtTokens;

class User extends Authenticatable
{
    use HasJwtTokens;
}
```

```
$request->user()->token();
$request->user()->tokenCan('invoices.read');
```

Refresh and Revocation
----------------------

[](#refresh-and-revocation)

Refresh tokens are returned as `id.secret`. Only the HMAC hash of the secret is stored.

```
$pair = app(JwtTokenService::class)->rotateRefreshToken(
    $request->input('refresh_token'),
);
```

Revoke one access token, one session, or all sessions for a user:

```
$token = $request->user()->token();

app(JwtTokenService::class)->revokeAccessToken($token->id);
app(JwtTokenService::class)->revokeSession($token->session_id);
app(JwtTokenService::class)->revokeAllForUser($request->user());
```

Account Security
----------------

[](#account-security)

Account security brokers can be called from controllers, Livewire actions, queued jobs, or service classes. Delivery is app-owned through sender contracts.

```
use Sopheak\JwtAuth\DTO\OtpDestination;
use Sopheak\JwtAuth\Services\EmailVerificationBroker;
use Sopheak\JwtAuth\Services\MfaChallengeBroker;
use Sopheak\JwtAuth\Services\OtpChallengeBroker;
use Sopheak\JwtAuth\Services\PasswordResetBroker;

$challenge = app(MfaChallengeBroker::class)->create($user, TokenContext::make());

$otp = app(OtpChallengeBroker::class)->createOtp(
    $challenge,
    new OtpDestination('email', 'user@example.com', 'u***@example.com'),
);

$context = app(OtpChallengeBroker::class)->verifyOtp($challenge->id, $otp->plaintextCode);

$verification = app(EmailVerificationBroker::class)
    ->createVerificationToken($user, $user->email);

$verified = app(EmailVerificationBroker::class)
    ->verifyEmailToken($verification->token);

$reset = app(PasswordResetBroker::class)->createResetToken($user, $user->email);
$result = app(PasswordResetBroker::class)->consumeResetToken($reset->token);
```

Available sender contracts:

- `OtpChannelSender`
- `EmailVerificationSender`
- `PasswordResetSender`

API Keys
--------

[](#api-keys)

API keys are for third-party integrations and machine clients. The full plaintext key is returned only at creation or rotation time.

```
use Sopheak\JwtAuth\DTO\ApiKeyContext;
use Sopheak\JwtAuth\Services\ApiKeyService;

$key = app(ApiKeyService::class)->createApiKey(ApiKeyContext::forCompany(
    companyId: 42,
    name: 'QuickBooks sync worker',
    scopes: ['qbo.sync', 'invoices.write'],
));
```

Protect integration routes:

```
Route::middleware(['sp.api_key', 'sp.api_key.scope:invoices.write'])
    ->post('/integrations/invoices', IntegrationInvoiceController::class);
```

Rotate or revoke:

```
$rotated = app(ApiKeyService::class)->rotateApiKey($apiKeyId);
app(ApiKeyService::class)->revokeApiKey($apiKeyId);
app(ApiKeyService::class)->revokeApiKeysForOwner('tenant', '42');
```

External Identity
-----------------

[](#external-identity)

External identity support normalizes provider profiles. The app decides whether to link, create, or deny a local user.

```
use Sopheak\JwtAuth\DTO\ExternalIdentity;
use Sopheak\JwtAuth\Services\ExternalIdentityStore;

app(ExternalIdentityStore::class)->store(new ExternalIdentity(
    provider: 'google',
    providerUserId: $providerUser->getId(),
    email: $providerUser->getEmail(),
    emailVerified: true,
    name: $providerUser->getName(),
    rawProfile: $providerUser->user,
), $user);
```

Provider adapters can implement `Sopheak\JwtAuth\Contracts\ExternalIdentityProvider`.

OAuth Server Mode
-----------------

[](#oauth-server-mode)

OAuth server mode is disabled by default and uses separate `sp_oauth_*` tables. It is for third-party clients, not normal first-party SPA/mobile login.

```
SP_JWT_OAUTH_SERVER_ENABLED=true
```

Create a client:

```
use Sopheak\JwtAuth\DTO\OAuthClientData;
use Sopheak\JwtAuth\Services\OAuthClientRepository;

$client = app(OAuthClientRepository::class)->createClient(new OAuthClientData(
    name: 'ERP Connector',
    redirectUris: ['https://client.example/callback'],
    allowedGrants: ['authorization_code', 'refresh_token'],
    allowedScopes: ['invoices.read'],
));
```

Protect OAuth resource routes:

```
Route::middleware(['sp.oauth', 'sp.oauth.scope:invoices.read'])
    ->get('/partner/invoices', PartnerInvoiceController::class);
```

OAuth client-credentials tokens authenticate as clients, not users.

Middleware
----------

[](#middleware)

MiddlewarePurpose`sp.jwt`Authenticate with the configured first-party JWT guard`sp.jwt.scope:`Require every listed JWT scope`sp.jwt.any_scope:,`Require any listed JWT scope`sp.api_key`Authenticate an API key bearer token`sp.api_key.scope:`Require every listed API key scope`sp.api_key.any_scope:,`Require any listed API key scope`sp.oauth`Authenticate an OAuth resource token`sp.oauth.scope:`Require every listed OAuth scope`sp.oauth.any_scope:,`Require any listed OAuth scope`sp.oauth.client:`Restrict OAuth access to a client idCommands
--------

[](#commands)

```
php artisan sp-jwt-auth:install --keys
php artisan sp-jwt-auth:setup --keys
php artisan sp-jwt-auth:validate
php artisan sp-jwt-auth:keys --generate --kid=2026-06-primary
php artisan sp-jwt-auth:jwks --pretty
php artisan sp-jwt-auth:prune --expired-days=30 --revoked-days=30
```

`sp-jwt-auth:keys --generate` and `--rotate` update `.env` by default with `SP_JWT_ACTIVE_KID`, `SP_JWT_PRIVATE_KEY_PATH`, and `SP_JWT_PUBLIC_KEY_PATH`. They also create `SP_JWT_REFRESH_HASH_KEY` when it is missing, without replacing an existing refresh hash secret. Use `--no-write-env` when your deployment manages environment values outside Artisan.

Events and Hooks
----------------

[](#events-and-hooks)

The package emits lifecycle events for:

- Token issue, refresh, revocation, sessions, and refresh reuse detection.
- MFA, OTP, email verification, and password reset.
- API key creation, use, revocation, and rotation.
- External identity resolution.
- OAuth clients, consents, authorization approval, token issue, and token revocation.

`HookRegistry` supports token-context validation, token-context mutation, and after-issue hooks for app-owned policy.

Security Notes
--------------

[](#security-notes)

- JWTs are signed with package signing keys, never `APP_KEY`.
- JWKS exposes public keys only.
- Refresh tokens, OTP codes, verification tokens, reset tokens, API keys, OAuth client secrets, and OAuth opaque tokens are stored as HMAC hashes.
- Refresh rotation runs in a transaction and detects reuse.
- OAuth tokens use separate storage and middleware from first-party JWT tokens.
- Optional modules are disabled by default and can be enabled incrementally.

Documentation
-------------

[](#documentation)

- [Getting started](docs/getting-started/installation.md)
- [Quick start](docs/getting-started/quick-start.md)
- [Auth controller example](docs/tutorials/auth-controller.md)
- [SPA and mobile integration](docs/tutorials/spa-mobile-integration.md)
- [API key client usage](docs/tutorials/api-key-client-usage.md)
- [OAuth authorization code](docs/tutorials/oauth-server-authorization-code.md)
- [OAuth client credentials](docs/tutorials/oauth-server-client-credentials.md)
- [MFA login](docs/tutorials/login-with-mfa.md)
- [Core JWT](docs/core-concepts/core-jwt.md)
- [Configuration](docs/core-concepts/configuration.md)
- [Account security](docs/features/mfa-otp.md)
- [API keys](docs/features/api-keys.md)
- [External identity](docs/features/external-identity.md)
- [OAuth server](docs/features/oauth-server.md)
- [Events and hooks](docs/features/events-hooks.md)
- [Middleware](docs/features/middleware.md)

Development
-----------

[](#development)

```
composer install
composer quality
```

`composer quality` runs Rector dry-run, PHPStan, and PHPUnit.

Release
-------

[](#release)

Packagist versions are created from Git tags:

```
git tag v1.0.0
git push origin v1.0.0
```

Packagist is already configured at [packagist.org/packages/sopheak/sp-jwt-auth](https://packagist.org/packages/sopheak/sp-jwt-auth). After pushing a new tag, Packagist makes the release available to Composer.

Community
---------

[](#community)

- Use GitHub Issues for reproducible bugs and focused feature requests.
- Use GitHub Discussions for questions, roadmap ideas, and integration help.
- See [SUPPORT.md](SUPPORT.md) for support channels and security boundaries.
- Report vulnerabilities through GitHub Security Advisories or the process in [SECURITY.md](SECURITY.md).
- Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE\_OF\_CONDUCT.md](CODE_OF_CONDUCT.md).

License
-------

[](#license)

This package is open-source software licensed under the [MIT license](LICENSE).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance95

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 86.4% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

9

Last Release

45d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/6a0e8420efb79ce87b09b2eaabcf48e14958badbbea83f26240dcbe5f3980f08?d=identicon)[sopheaksem9999](/maintainers/sopheaksem9999)

---

Top Contributors

[![sopheaksem9999](https://avatars.githubusercontent.com/u/80437267?v=4)](https://github.com/sopheaksem9999 "sopheaksem9999 (19 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (3 commits)")

---

Tags

authenticationjwtjwt-authenticationmfaoidcotp-verificationtokenjwtlaravelAuthenticationoauthapi-key

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/sopheak-sp-jwt-auth/health.svg)

```
[![Health](https://phpackages.com/badges/sopheak-sp-jwt-auth/health.svg)](https://phpackages.com/packages/sopheak-sp-jwt-auth)
```

###  Alternatives

[laravel/octane

Supercharge your Laravel application's performance.

4.0k26.6M234](/packages/laravel-octane)[statamic/cms

The Statamic CMS Core Package

4.8k3.6M1.1k](/packages/statamic-cms)[ellaisys/aws-cognito

Laravel Authentication using AWS Cognito (Web and API)

121256.9k1](/packages/ellaisys-aws-cognito)[php-open-source-saver/jwt-auth

JSON Web Token Authentication for Laravel and Lumen

84611.1M64](/packages/php-open-source-saver-jwt-auth)[auth0/login

Auth0 Laravel SDK. Straight-forward and tested methods for implementing authentication, and accessing Auth0's Management API endpoints.

2745.3M3](/packages/auth0-login)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
