{"id":50870802,"url":"https://github.com/sopheaksem9999/sp-jwt-auth","last_synced_at":"2026-06-15T05:01:31.989Z","repository":{"id":364910432,"uuid":"1266320881","full_name":"sopheaksem9999/sp-jwt-auth","owner":"sopheaksem9999","description":"SP JWT Auth Package JWT Authentication Package for Laravel","archived":false,"fork":false,"pushed_at":"2026-06-15T04:17:48.000Z","size":347,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-15T04:21:43.595Z","etag":null,"topics":["authentication","jwt","jwt-authentication","mfa","oidc","otp-verification","token"],"latest_commit_sha":null,"homepage":"https://sp-jwt-auth-docs.vercel.app/","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sopheaksem9999.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":"SUPPORT.md","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-11T14:07:26.000Z","updated_at":"2026-06-15T04:20:22.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sopheaksem9999/sp-jwt-auth","commit_stats":null,"previous_names":["sopheaksem9999/sp-jwt-auth"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/sopheaksem9999/sp-jwt-auth","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sopheaksem9999%2Fsp-jwt-auth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sopheaksem9999%2Fsp-jwt-auth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sopheaksem9999%2Fsp-jwt-auth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sopheaksem9999%2Fsp-jwt-auth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sopheaksem9999","download_url":"https://codeload.github.com/sopheaksem9999/sp-jwt-auth/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sopheaksem9999%2Fsp-jwt-auth/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34348292,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","jwt","jwt-authentication","mfa","oidc","otp-verification","token"],"created_at":"2026-06-15T05:01:09.381Z","updated_at":"2026-06-15T05:01:31.982Z","avatar_url":"https://github.com/sopheaksem9999.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SP JWT Auth\n\n[![Latest Stable Version](https://img.shields.io/packagist/v/sopheak/sp-jwt-auth.svg)](https://packagist.org/packages/sopheak/sp-jwt-auth)\n[![Total Downloads](https://img.shields.io/packagist/dt/sopheak/sp-jwt-auth.svg)](https://packagist.org/packages/sopheak/sp-jwt-auth)\n[![PHP Version](https://img.shields.io/packagist/dependency-v/sopheak/sp-jwt-auth/php.svg)](composer.json)\n[![License](https://img.shields.io/packagist/l/sopheak/sp-jwt-auth.svg)](LICENSE)\n[![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)\n\n`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.\n\nThe package owns authentication infrastructure. Your application still owns password login, registration, user creation, tenants, roles, UI, response shape, delivery templates, and business authorization policy.\n\n## Features\n\n| Module | What it provides | Default |\n| --- | --- | --- |\n| Core JWT | `sp-jwt` guard, signed JWT access tokens, persisted `jti`, opaque rotating refresh tokens, scopes, claims, revocation, key rotation, JWKS, events, hooks | Enabled |\n| Account Security | MFA challenge broker, hashed OTP codes, email verification tokens, password reset tokens, app-owned sender contracts | Disabled |\n| API Keys | Scoped integration keys with public-id lookup, HMAC secret validation, rotation, revocation, IP restrictions, middleware | Disabled |\n| External Identity | Normalized Socialite/OIDC-style identity DTO, provider contract, external identity storage | Disabled |\n| OAuth Server | Separate `sp_oauth_*` storage, clients, consents, authorization-code + PKCE, refresh tokens, client credentials, revocation, introspection, resource middleware | Disabled |\n\n## Requirements\n\n- PHP `^8.3|^8.4|^8.5`\n- Laravel `^12.0|^13.0`\n- `firebase/php-jwt`\n- RSA signing keys for the default `RS256` setup\n\nOptional integrations are kept in Composer `suggest`:\n\n- `laravel/socialite`\n- `socialiteproviders/manager`\n- `league/oauth2-client`\n- `league/oauth2-server`\n\n## Stability\n\nThis 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.\n\n## Installation\n\nThe package is public on Packagist: [sopheak/sp-jwt-auth](https://packagist.org/packages/sopheak/sp-jwt-auth).\n\nInstall it with Composer:\n\n```bash\ncomposer require sopheak/sp-jwt-auth\nphp artisan sp-jwt-auth:setup --keys\nphp artisan migrate\nphp artisan sp-jwt-auth:validate\n```\n\nThe 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:\n\n```php\n'guards' =\u003e [\n    'api' =\u003e [\n        'driver' =\u003e 'sp-jwt',\n        'provider' =\u003e 'users',\n    ],\n],\n```\n\nKeep Laravel's normal `web` guard for Blade, Livewire, Inertia, and session pages.\n\nFor local path testing while developing the package:\n\n```bash\ncomposer config repositories.sp-jwt-auth '{\"type\":\"path\",\"url\":\"/absolute/path/to/sp-jwt-auth\",\"options\":{\"versions\":{\"sopheak/sp-jwt-auth\":\"0.1.0\"}}}'\ncomposer require sopheak/sp-jwt-auth:^0.1\n```\n\n## Configuration\n\nPublish the config when needed:\n\n```bash\nphp artisan vendor:publish --tag=sp-jwt-auth-config\n```\n\nCommon environment keys:\n\n```env\nSP_JWT_GUARD=api\nSP_JWT_USER_PROVIDER=users\nSP_JWT_ISSUER=https://app.example.com\nSP_JWT_AUDIENCE=app-api\nSP_JWT_ALGORITHM=RS256\nSP_JWT_ACCESS_TTL_MINUTES=15\nSP_JWT_REFRESH_TTL_DAYS=60\nSP_JWT_REUSE_DETECTION=revoke_session\nSP_JWT_ACTIVE_KID=2026-06-primary\nSP_JWT_PRIVATE_KEY_PATH=storage/jwt-private-2026-06-primary.pem\nSP_JWT_PUBLIC_KEY_PATH=storage/jwt-public-2026-06-primary.pem\nSP_JWT_HASH_KEY_ID=default\nSP_JWT_REFRESH_HASH_KEY=your-random-refresh-hash-secret\n```\n\nOptional modules have their own config sections:\n\n- `mfa`\n- `email_verification`\n- `password_reset`\n- `api_keys`\n- `external_identities`\n- `oauth_server`\n\n## Quick Start\n\nCreate login and refresh endpoints in your Laravel app. Your app owns credential validation; the package owns token issuing, refresh rotation, and token response formatting.\n\n```php\nuse App\\Models\\User;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Hash;\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Validation\\ValidationException;\nuse Sopheak\\JwtAuth\\DTO\\TokenContext;\nuse Sopheak\\JwtAuth\\Services\\JwtTokenService;\nuse Sopheak\\JwtAuth\\Support\\TokenResponse;\n\nRoute::post('/login', function (Request $request, JwtTokenService $jwt) {\n    $credentials = $request-\u003evalidate([\n        'email' =\u003e ['required', 'email'],\n        'password' =\u003e ['required', 'string'],\n    ]);\n\n    $user = User::query()-\u003ewhere('email', $credentials['email'])-\u003efirst();\n\n    if (! $user || ! Hash::check($credentials['password'], $user-\u003epassword)) {\n        throw ValidationException::withMessages([\n            'email' =\u003e ['The provided credentials are incorrect.'],\n        ]);\n    }\n\n    $pair = $jwt-\u003eissueTokenPair(\n        $user,\n        TokenContext::make()-\u003escopes(['profile.read']),\n    );\n\n    return TokenResponse::passportCompatible($pair);\n});\n\nRoute::post('/refresh', function (Request $request, JwtTokenService $jwt) {\n    $data = $request-\u003evalidate([\n        'refresh_token' =\u003e ['required', 'string'],\n    ]);\n\n    return TokenResponse::passportCompatible(\n        $jwt-\u003erotateRefreshToken($data['refresh_token']),\n    );\n});\n\nRoute::middleware('auth:api')-\u003eget('/me', function (Request $request) {\n    return $request-\u003euser();\n});\n```\n\nCall protected routes with the returned access token:\n\n```http\nAuthorization: Bearer \u003caccess-token\u003e\n```\n\n## Core JWT Usage\n\nYour app validates credentials, resolves a user, builds a `TokenContext`, then asks the package to issue tokens.\n\n```php\nuse Sopheak\\JwtAuth\\DTO\\TokenContext;\nuse Sopheak\\JwtAuth\\Services\\JwtTokenService;\nuse Sopheak\\JwtAuth\\Support\\TokenResponse;\n\n$pair = app(JwtTokenService::class)-\u003eissueTokenPair(\n    $user,\n    TokenContext::make()\n        -\u003ecompanyId(42)\n        -\u003ecompanyIds([42, 84])\n        -\u003escopes(['invoices.read', 'invoices.write'])\n        -\u003eimpersonated(false),\n);\n\nreturn TokenResponse::passportCompatible($pair);\n```\n\nRead claims from the authenticated token:\n\n```php\n$token = $request-\u003euser()?-\u003etoken();\n\n$companyId = $token?-\u003eclaim('company_id');\n$claims = $token?-\u003eclaims ?? [];\n```\n\nFor response fields owned by the app, pass extra data to the response helper or register a response extension:\n\n```php\nreturn TokenResponse::passportCompatible($pair, [\n    'company_id' =\u003e $pair-\u003eaccessTokenRecord-\u003ecompanyId(),\n]);\n\nTokenResponse::extend(function (array $response, TokenPair $pair): array {\n    $response['company_id'] = $pair-\u003eaccessTokenRecord-\u003ecompanyId();\n    $response['impersonated'] = $pair-\u003eaccessTokenRecord-\u003eisImpersonated();\n\n    return $response;\n});\n```\n\nProtect routes with Laravel auth middleware:\n\n```php\nRoute::middleware(['auth:api'])-\u003eget('/me', MeController::class);\n\nRoute::middleware(['auth:api', 'sp.jwt.scope:invoices.read'])\n    -\u003eget('/invoices', InvoiceIndexController::class);\n```\n\nAdd Passport-like helpers to user models:\n\n```php\nuse Sopheak\\JwtAuth\\Traits\\HasJwtTokens;\n\nclass User extends Authenticatable\n{\n    use HasJwtTokens;\n}\n```\n\n```php\n$request-\u003euser()-\u003etoken();\n$request-\u003euser()-\u003etokenCan('invoices.read');\n```\n\n## Refresh and Revocation\n\nRefresh tokens are returned as `id.secret`. Only the HMAC hash of the secret is stored.\n\n```php\n$pair = app(JwtTokenService::class)-\u003erotateRefreshToken(\n    $request-\u003einput('refresh_token'),\n);\n```\n\nRevoke one access token, one session, or all sessions for a user:\n\n```php\n$token = $request-\u003euser()-\u003etoken();\n\napp(JwtTokenService::class)-\u003erevokeAccessToken($token-\u003eid);\napp(JwtTokenService::class)-\u003erevokeSession($token-\u003esession_id);\napp(JwtTokenService::class)-\u003erevokeAllForUser($request-\u003euser());\n```\n\n## Account Security\n\nAccount security brokers can be called from controllers, Livewire actions, queued jobs, or service classes. Delivery is app-owned through sender contracts.\n\n```php\nuse Sopheak\\JwtAuth\\DTO\\OtpDestination;\nuse Sopheak\\JwtAuth\\Services\\EmailVerificationBroker;\nuse Sopheak\\JwtAuth\\Services\\MfaChallengeBroker;\nuse Sopheak\\JwtAuth\\Services\\OtpChallengeBroker;\nuse Sopheak\\JwtAuth\\Services\\PasswordResetBroker;\n\n$challenge = app(MfaChallengeBroker::class)-\u003ecreate($user, TokenContext::make());\n\n$otp = app(OtpChallengeBroker::class)-\u003ecreateOtp(\n    $challenge,\n    new OtpDestination('email', 'user@example.com', 'u***@example.com'),\n);\n\n$context = app(OtpChallengeBroker::class)-\u003everifyOtp($challenge-\u003eid, $otp-\u003eplaintextCode);\n\n$verification = app(EmailVerificationBroker::class)\n    -\u003ecreateVerificationToken($user, $user-\u003eemail);\n\n$verified = app(EmailVerificationBroker::class)\n    -\u003everifyEmailToken($verification-\u003etoken);\n\n$reset = app(PasswordResetBroker::class)-\u003ecreateResetToken($user, $user-\u003eemail);\n$result = app(PasswordResetBroker::class)-\u003econsumeResetToken($reset-\u003etoken);\n```\n\nAvailable sender contracts:\n\n- `OtpChannelSender`\n- `EmailVerificationSender`\n- `PasswordResetSender`\n\n## API Keys\n\nAPI keys are for third-party integrations and machine clients. The full plaintext key is returned only at creation or rotation time.\n\n```php\nuse Sopheak\\JwtAuth\\DTO\\ApiKeyContext;\nuse Sopheak\\JwtAuth\\Services\\ApiKeyService;\n\n$key = app(ApiKeyService::class)-\u003ecreateApiKey(ApiKeyContext::forCompany(\n    companyId: 42,\n    name: 'QuickBooks sync worker',\n    scopes: ['qbo.sync', 'invoices.write'],\n));\n```\n\nProtect integration routes:\n\n```php\nRoute::middleware(['sp.api_key', 'sp.api_key.scope:invoices.write'])\n    -\u003epost('/integrations/invoices', IntegrationInvoiceController::class);\n```\n\nRotate or revoke:\n\n```php\n$rotated = app(ApiKeyService::class)-\u003erotateApiKey($apiKeyId);\napp(ApiKeyService::class)-\u003erevokeApiKey($apiKeyId);\napp(ApiKeyService::class)-\u003erevokeApiKeysForOwner('tenant', '42');\n```\n\n## External Identity\n\nExternal identity support normalizes provider profiles. The app decides whether to link, create, or deny a local user.\n\n```php\nuse Sopheak\\JwtAuth\\DTO\\ExternalIdentity;\nuse Sopheak\\JwtAuth\\Services\\ExternalIdentityStore;\n\napp(ExternalIdentityStore::class)-\u003estore(new ExternalIdentity(\n    provider: 'google',\n    providerUserId: $providerUser-\u003egetId(),\n    email: $providerUser-\u003egetEmail(),\n    emailVerified: true,\n    name: $providerUser-\u003egetName(),\n    rawProfile: $providerUser-\u003euser,\n), $user);\n```\n\nProvider adapters can implement `Sopheak\\JwtAuth\\Contracts\\ExternalIdentityProvider`.\n\n## OAuth Server Mode\n\nOAuth 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.\n\n```env\nSP_JWT_OAUTH_SERVER_ENABLED=true\n```\n\nCreate a client:\n\n```php\nuse Sopheak\\JwtAuth\\DTO\\OAuthClientData;\nuse Sopheak\\JwtAuth\\Services\\OAuthClientRepository;\n\n$client = app(OAuthClientRepository::class)-\u003ecreateClient(new OAuthClientData(\n    name: 'ERP Connector',\n    redirectUris: ['https://client.example/callback'],\n    allowedGrants: ['authorization_code', 'refresh_token'],\n    allowedScopes: ['invoices.read'],\n));\n```\n\nProtect OAuth resource routes:\n\n```php\nRoute::middleware(['sp.oauth', 'sp.oauth.scope:invoices.read'])\n    -\u003eget('/partner/invoices', PartnerInvoiceController::class);\n```\n\nOAuth client-credentials tokens authenticate as clients, not users.\n\n## Middleware\n\n| Middleware | Purpose |\n| --- | --- |\n| `sp.jwt` | Authenticate with the configured first-party JWT guard |\n| `sp.jwt.scope:\u003cscope\u003e` | Require every listed JWT scope |\n| `sp.jwt.any_scope:\u003cscope1\u003e,\u003cscope2\u003e` | Require any listed JWT scope |\n| `sp.api_key` | Authenticate an API key bearer token |\n| `sp.api_key.scope:\u003cscope\u003e` | Require every listed API key scope |\n| `sp.api_key.any_scope:\u003cscope1\u003e,\u003cscope2\u003e` | Require any listed API key scope |\n| `sp.oauth` | Authenticate an OAuth resource token |\n| `sp.oauth.scope:\u003cscope\u003e` | Require every listed OAuth scope |\n| `sp.oauth.any_scope:\u003cscope1\u003e,\u003cscope2\u003e` | Require any listed OAuth scope |\n| `sp.oauth.client:\u003cclient_id\u003e` | Restrict OAuth access to a client id |\n\n## Commands\n\n```bash\nphp artisan sp-jwt-auth:install --keys\nphp artisan sp-jwt-auth:setup --keys\nphp artisan sp-jwt-auth:validate\nphp artisan sp-jwt-auth:keys --generate --kid=2026-06-primary\nphp artisan sp-jwt-auth:jwks --pretty\nphp artisan sp-jwt-auth:prune --expired-days=30 --revoked-days=30\n```\n\n`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.\n\n## Events and Hooks\n\nThe package emits lifecycle events for:\n\n- Token issue, refresh, revocation, sessions, and refresh reuse detection.\n- MFA, OTP, email verification, and password reset.\n- API key creation, use, revocation, and rotation.\n- External identity resolution.\n- OAuth clients, consents, authorization approval, token issue, and token revocation.\n\n`HookRegistry` supports token-context validation, token-context mutation, and after-issue hooks for app-owned policy.\n\n## Security Notes\n\n- JWTs are signed with package signing keys, never `APP_KEY`.\n- JWKS exposes public keys only.\n- Refresh tokens, OTP codes, verification tokens, reset tokens, API keys, OAuth client secrets, and OAuth opaque tokens are stored as HMAC hashes.\n- Refresh rotation runs in a transaction and detects reuse.\n- OAuth tokens use separate storage and middleware from first-party JWT tokens.\n- Optional modules are disabled by default and can be enabled incrementally.\n\n## Documentation\n\n- [Getting started](docs/getting-started/installation.md)\n- [Quick start](docs/getting-started/quick-start.md)\n- [Auth controller example](docs/tutorials/auth-controller.md)\n- [SPA and mobile integration](docs/tutorials/spa-mobile-integration.md)\n- [API key client usage](docs/tutorials/api-key-client-usage.md)\n- [OAuth authorization code](docs/tutorials/oauth-server-authorization-code.md)\n- [OAuth client credentials](docs/tutorials/oauth-server-client-credentials.md)\n- [MFA login](docs/tutorials/login-with-mfa.md)\n- [Core JWT](docs/core-concepts/core-jwt.md)\n- [Configuration](docs/core-concepts/configuration.md)\n- [Account security](docs/features/mfa-otp.md)\n- [API keys](docs/features/api-keys.md)\n- [External identity](docs/features/external-identity.md)\n- [OAuth server](docs/features/oauth-server.md)\n- [Events and hooks](docs/features/events-hooks.md)\n- [Middleware](docs/features/middleware.md)\n\n## Development\n\n```bash\ncomposer install\ncomposer quality\n```\n\n`composer quality` runs Rector dry-run, PHPStan, and PHPUnit.\n\n## Release\n\nPackagist versions are created from Git tags:\n\n```bash\ngit tag v1.0.0\ngit push origin v1.0.0\n```\n\nPackagist 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.\n\n## Community\n\n- Use GitHub Issues for reproducible bugs and focused feature requests.\n- Use GitHub Discussions for questions, roadmap ideas, and integration help.\n- See [SUPPORT.md](SUPPORT.md) for support channels and security boundaries.\n- Report vulnerabilities through GitHub Security Advisories or the process in [SECURITY.md](SECURITY.md).\n- Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).\n\n## License\n\nThis package is open-source software licensed under the [MIT license](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsopheaksem9999%2Fsp-jwt-auth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsopheaksem9999%2Fsp-jwt-auth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsopheaksem9999%2Fsp-jwt-auth/lists"}