PHP and Laravel
Sign users in from PHP and Laravel with the 9in packages or any OpenID Connect client.
PHP apps sign users in as a confidential client (see Server-rendered apps for how that works). Use the 9in packages, or any OpenID Connect client such as jumbojett/openid-connect-php.
Create the application first: Applications → Create application → Regular Web App, with your callback URL (for Laravel, https://your-app.com/nine/callback) and logout URL, and copy the client ID and secret.
#Laravel
composer require 9in/laravel:^3.0NINE_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 APIProtect pages with the nine:redirect middleware:
Route::middleware(['web', 'nine:redirect'])->group(function () {
Route::get('/dashboard', function () {
$user = auth()->user(); // id (= sub), email, name, … from the ID token
return view('dashboard', compact('user'));
});
});
route('nine.login', ['return_to' => '/dashboard']); // add 'screen' => 'signup' for the sign-up form
route('nine.logout');| Middleware | When signed out |
|---|---|
nine | 401 JSON |
nine:redirect | Browsers go to the 9in sign-in page 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 (use after nine) |
Protect an API with the same guard. Tokens must be issued for NINE_API_AUDIENCE (defaults to NINE_AUDIENCE):
Route::middleware(['nine', 'nine.scope:orders:read'])->get('/api/orders', function () {
$sub = auth()->id(); // 9in user id
$claims = auth('nine')->claims(); // client_id, scope, aud, …
});With the client ID and secret set, the guard verifies tokens in hybrid mode (local check, then one cached introspection), and the package's /nine/webhook route drops cached answers when it receives token.revoked, session.revoked or user.* events; set NINE_WEBHOOK_SECRET and subscribe that URL in the console. Publish the config with php artisan vendor:publish --tag=nine:config to change routes, scopes or caching.
To keep your own users table, add a nine_id column and point the guard at your provider:
// config/auth.php
'guards' => [
'nine' => ['driver' => 'nine', 'provider' => 'users', 'local_key' => 'nine_id'],
],#Plain PHP
composer require 9in/php-sdk:^3.0use Nine\OAuth2Client;
use Nine\OAuth2Config;
use Nine\Storage\FileStorage;
$client = new OAuth2Client(OAuth2Config::fromEnv([
'redirect_uri' => 'https://app.acme.com/callback',
'state_store' => new FileStorage('/var/lib/myapp/9in'), // pending logins (PKCE verifier, nonce)
]));
// GET /login
$login = $client->createAuthorizationUrl(['return_to' => '/dashboard']); // or ['screen' => 'signup']
header('Location: ' . $login['url']);
// GET /callback
$tokens = $client->handleCallback($_GET); // checks state + iss, exchanges the code, verifies the ID token
$_SESSION['9in'] = $tokens->toArray();
header('Location: ' . ($tokens->returnTo ?? '/'));
// later
$tokens = $client->ensureFresh(\Nine\TokenSet::fromArray($_SESSION['9in'])); // refreshes (and rotates)
// GET /logout
$client->revoke($tokens->refreshToken);
header('Location: ' . $client->logoutUrl($tokens->idToken, 'https://app.acme.com/'));OAuth2Config::fromEnv() reads NINE_ISSUER, NINE_CLIENT_ID, NINE_CLIENT_SECRET and NINE_REDIRECT_URI. The same package has TokenVerifier and HybridTokenVerifier for APIs, ManagementClient, webhook verification and an adapter handler for your own database.
#Any OpenID Connect library
$oidc = new Jumbojett\OpenIDConnectClient('https://acme.9in.io', 'app_…', getenv('NINE_CLIENT_SECRET'));
$oidc->setRedirectURL('https://app.acme.com/callback');
$oidc->addScope(['openid', 'profile', 'email']);
$oidc->setCodeChallengeMethod('S256');
$oidc->authenticate(); // redirects, then handles the callback
$user = $oidc->requestUserInfo(); // sub, email, name, …#Coming from SSOfy's packages
The 9in packages replace ssofy/*. They still read the old SSOFY_URL, SSOFY_CLIENT_ID and SSOFY_CLIENT_SECRET when the NINE_* variables are missing. See Coming from SSOfy for what else changed.