# PHP and Laravel

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

Source: https://9in.io/docs/php-laravel/

PHP apps sign users in as a **confidential client** (see [Server-rendered apps](https://9in.io/docs/server-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');
```

| 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`):

```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](https://9in.io/docs/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](https://9in.io/docs/coming-from-ssofy/) for what else changed.
