Build your app

PHP and Laravel

Sign users in from PHP and Laravel with the 9in packages or any OpenID Connect client.

View .md llms.txt

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

bash
composer require 9in/laravel:^3.0
dotenv
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

Protect pages with the nine:redirect middleware:

php
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');
MiddlewareWhen signed out
nine401 JSON
nine:redirectBrowsers go to the 9in sign-in page and come back; API requests get 401
nine:passiveContinue; auth('nine')->user() is null
nine.scope:orders:read,orders:write403 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):

php
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:

php
// config/auth.php
'guards' => [
    'nine' => ['driver' => 'nine', 'provider' => 'users', 'local_key' => 'nine_id'],
],

#Plain PHP

bash
composer require 9in/php-sdk:^3.0
php
use 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

php
$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.