ssofy / laravel
Sign users in to Laravel with 9in: OpenID Connect login with PKCE, a `nine` auth guard with local JWT verification for APIs, and verified webhooks.
Requires
- php: >=8.1
- 9in/laravel-events: ^2.0
- 9in/php-sdk: ^3.0
- laravel/framework: ^10.0 || ^11.0 || ^12.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.11
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Sign users in to your Laravel app with 9in, protect your API with 9in access tokens, and receive verified webhooks.
- Browser sign-in: OpenID Connect authorization code flow with PKCE (
/nine/login,/nine/callback,/nine/logout). The ID token is verified, tokens are kept in the Laravel session and refreshed (with rotation) when they expire. - API tokens:
Authorization: Bearer …is verified locally against your tenant's signing keys (cached in Laravel's cache): no call to 9in per request. nineguard and middleware, scope checks, and aNinefacade.- Webhooks at
/nine/webhook, dispatched as Laravel events.
Requires PHP 8.1+ and Laravel 10, 11 or 12.
composer require 9in/laravel:^3.0
Upgrading from ssofy/laravel 2.x? Read MIGRATION.md.
Setup
Create an application in the 9in console (Applications → New → Regular web app) with the
redirect URL https://your-app.com/nine/callback and the logout URL
https://your-app.com/, then:
NINE_ISSUER=https://acme.9in.io # or your custom login domain NINE_CLIENT_ID=app_… NINE_CLIENT_SECRET=… # NINE_AUDIENCE=https://api.acme.com # access tokens for your own API
The old SSOFY_URL, SSOFY_CLIENT_ID and SSOFY_CLIENT_SECRET are still read when the
NINE_* names are not set. The issuer must use https://; plain http is accepted for
localhost and *.localhost (set NINE_ALLOW_INSECURE_ISSUER=true for other development
hosts). Publish the config with
php artisan vendor:publish --tag=nine:config to change routes, scopes or caching.
Protect pages
Route::middleware(['web', 'nine:redirect'])->group(function () { Route::get('/dashboard', function () { $user = auth()->user(); // Nine\Laravel\User: id (= sub), email, name, … from the ID token return view('dashboard', compact('user')); }); }); // Links route('nine.login', ['return_to' => '/dashboard']); // add 'screen' => 'signup' for the sign-up form route('nine.logout');
| Middleware | Signed out |
|---|---|
nine |
401 JSON |
nine:redirect |
Browsers go to the 9in login and come back; API requests get 401 |
nine:passive |
Continue; auth('nine')->user() is null |
nine.scope:orders:read,orders:write |
403 unless the bearer token has every scope (after nine) |
The nine guard is registered automatically (auth('nine')); nine middleware also makes
it the default guard for the request.
Using your own users table
Add a column holding the 9in user id (nine_id) and point the guard at your provider in
config/auth.php:
'guards' => [ 'nine' => ['driver' => 'nine', 'provider' => 'users', 'local_key' => 'nine_id'], ],
auth()->user() is then your Eloquent user (null until you link one, e.g. in a
UserCreated webhook listener or on first sign-in using auth('nine')->claims()).
Protect your API
Route::middleware(['nine', 'nine.scope:orders:read'])->get('/api/orders', function () { $sub = auth()->id(); // 9in user id $claims = auth('nine')->claims(); // access-token claims: client_id, scope, aud, … });
Tokens must be issued for nine.api.audience (NINE_API_AUDIENCE, defaults to
NINE_AUDIENCE). Nine::verifyAccessToken($token, ['scope' => …]) does the same by hand.
- Every key-library algorithm is accepted: RS256/384/512, PS256/384/512, ES256, ES384,
EdDSA, and HS256/384/512 with the API's shared secret in
NINE_API_SECRET. Only access tokens (typ: at+jwt) are accepted, never ID tokens. - Revocations take effect at once. With client credentials configured (
NINE_CLIENT_ID/NINE_CLIENT_SECRET, or theNINE_MANAGEMENT_*pair), the guard runs in hybrid mode. Each token is checked locally, introspected once, and the answer is cached innine.cache.store(NINE_TOKEN_CACHE_TTL, default 300 s). When/nine/webhookreceivestoken.revoked,session.revokedor auser.*event, it drops the matching answers, so the next request is refused. Subscribe the webhook endpoint to those events. - Opaque access tokens (
9in_at_…) are introspected. - Choosing the mode:
NINE_TOKEN_VERIFICATION=localskips introspection (a revoked token works until it expires);introspectasks 9in on every request. Without client credentials the guard useslocal.
Webhooks
Create an endpoint in the console (Advanced → Webhooks) for
https://your-app.com/nine/webhook and set its signing secret:
NINE_WEBHOOK_SECRET=whsec_…
Verified events (signature over the raw body and path, at most five minutes old) are
dispatched as 9in/laravel-events classes:
use Nine\Laravel\Events\UserCreated; Event::listen(UserCreated::class, function (UserCreated $event) { $user = $event->user(); // 9in user snapshot // $event->id() is stable across retries: use it to ignore duplicates });
Each event is dispatched once: retries and redeliveries of a processed event id are
answered 204 without dispatching again, and a retry that arrives while the first delivery is
still running gets 409 (9in tries again later). If a listener throws, the id is released
and 9in retries. Ids are kept for 3 days in the nine.cache.store cache; use a shared store
(redis, database) when you run several servers. Turn it off with nine.webhook.dedupe.
To receive webhooks on a different route, use the nine.webhook middleware there and read
$request->attributes->get('nine.webhook').
Management API
use Nine\Laravel\Facades\Nine; Nine::management()->createUser(['email' => 'jane@acme.com', 'emailVerified' => true]); Nine::management()->revokeUserSessions($userId); $link = Nine::management()->createLoginLink($userId, ['returnTo' => url('/nine/callback')]);
The application needs management scopes. Set NINE_MANAGEMENT_CLIENT_ID /
NINE_MANAGEMENT_CLIENT_SECRET to use a separate machine-to-machine application.
Keeping users in your own database instead? Add
9in/laravel-server.
Tests
composer install
composer test
License
The MIT License (MIT). See LICENSE. 9in and SSOfy are by Cubelet Ltd.
