Aggiungi l'accesso a un'app Laravel
Questo quickstart aggiunge l'accesso con KeenAuth a un'app Laravel 13 usando il client OpenID Connect standard facile-it/php-openid-client. KeenAuth non ha ancora un SDK PHP, e non ne serve uno: KeenAuth è un provider OpenID Connect standard, quindi funziona una libreria client OpenID Connect standard. La tua app è il client OAuth e conserva ogni token nella sessione lato server; il browser riceve solo un cookie con l'id di sessione cifrato. Aggiungi le route di accesso, proteggi una pagina, chiami un'API protetta con l'access token dell'utente, disconnetti l'utente e testi l'app offline.
L'app ha queste route:
| Route | Cosa fa |
|---|---|
GET / |
Home page pubblica: un pulsante di accesso, oppure il nome dell'utente e un pulsante di disconnessione |
GET /dashboard |
Richiede una sessione. Mostra l'utente e i messaggi restituiti dalla tua API |
POST /login |
Avvia l'accesso. Laravel verifica il token CSRF |
GET /auth/callback |
Controlla la risposta, scambia il codice e avvia la sessione |
POST /logout |
Termina la sessione qui e presso KeenAuth |
Prima di iniziare
- Un ambiente KeenAuth. Il suo issuer ha la forma
https://your-environment.eu.keenauth.app. - Un'applicazione web nell'ambiente (Crea un'applicazione) con un client secret (
client_secret_basic). In un ambientedevelopment, registra l'URI di redirecthttp://localhost/auth/callbacke l'URI di redirect dopo la disconnessionehttp://localhost/; un URI loopback è registrato senza porta e corrisponde a qualsiasi porta, come8000. - Un'API registrata nell'ambiente (Crea un'API) con lo scope
messages:read. Il suoidentifier, per esempiohttps://api.example.com, è l'audience dei suoi access token. Il quickstart Express crea un'API che accetta questi token. - PHP 8.3 o successivo e Composer 2.
1. Installa
composer require facile-it/php-openid-client:1.0.0 guzzlehttp/guzzle:8.3.1
La libreria richiede un client HTTP PSR-18; Guzzle è una possibilità. L'esempio fissa ogni pacchetto con una versione esatta in composer.json, e composer.lock fissa ogni pacchetto a un commit. L'installazione avviene con composer install --no-plugins --no-scripts, quindi durante l'installazione non viene eseguito codice di alcun pacchetto.
2. Configura
L'app legge la sua configurazione solo da variabili d'ambiente, tramite config/keenauth.php. Inseriscile in .env, che Laravel carica, e non committare mai quel file.
| Variabile | Obbligatoria | Significato |
|---|---|---|
KEENAUTH_ISSUER |
sì | L'issuer del tuo ambiente, https |
KEENAUTH_CLIENT_ID |
sì | Il client_id dell'applicazione web |
KEENAUTH_CLIENT_SECRET |
sì | Il suo client secret |
APP_BASE_URL |
sì | L'URL pubblico dell'app. L'URI di redirect viene costruito a partire da esso. http solo per localhost, 127.0.0.1 o [::1] |
API_URL |
sì | L'API che l'app chiama, per esempio http://localhost:8080 |
KEENAUTH_API_AUDIENCE |
sì | L'identificatore di quell'API in KeenAuth |
KEENAUTH_ID_TOKEN_ALG |
no | L'algoritmo con cui il tuo ambiente firma gli ID token: RS256 (predefinito), PS256, ES256 o EdDSA |
APP_KEY |
sì | La chiave di Laravel; cifra il cookie di sessione. Generane una con php artisan key:generate |
Config::fromArray verifica questi valori. Un valore mancante o non utilizzabile blocca la prima richiesta che ne ha bisogno, e nessun messaggio di errore contiene un valore. Un algoritmo HMAC viene rifiutato, così il client secret non può mai firmare un ID token:
$alg = $values['id_token_alg'] ?? 'RS256';
if (!in_array($alg, ['RS256', 'PS256', 'ES256', 'EdDSA'], true)) {
// Never an HMAC algorithm: the client secret must not be able to sign an ID token.
throw new InvalidArgumentException('KEENAUTH_ID_TOKEN_ALG must be RS256, PS256, ES256 or EdDSA');
}
3. Crea il client OpenID Connect
App\KeenAuth\KeenAuth costruisce il client a partire dalla discovery (<issuer>/.well-known/openid-configuration) e mette in cache i metadati e il set di chiavi per un'ora:
$issuer = (new IssuerBuilder())
->setMetadataProviderBuilder((new MetadataProviderBuilder())->setHttpClient($this->http)
->setCache($this->cache)->setCacheTtl(3600))
->setJwksProviderBuilder((new JwksProviderBuilder())->withHttpClient($this->http)
->withCache($this->cache)->withCacheTtl(3600))
->build($this->config->issuer);
->setClientMetadata(ClientMetadata::fromArray([
'client_id' => $this->config->clientId,
'client_secret' => $this->config->clientSecret,
'token_endpoint_auth_method' => 'client_secret_basic',
'redirect_uris' => [$this->config->redirectUri()],
// The verifier accepts only this algorithm, never an HMAC one.
'id_token_signed_response_alg' => $this->config->idTokenAlg,
]))
Il service provider lo registra come singleton, e il client HTTP come binding separato, così che i test possano sostituirlo.
4. Avvia l'accesso
A ogni accesso vengono generati un nuovo state, nonce e verificatore PKCE, che vengono conservati nella sessione:
public static function newTransaction(): array
{
return [
'state' => self::base64url(random_bytes(32)),
'nonce' => self::base64url(random_bytes(32)),
'code_verifier' => self::base64url(random_bytes(32)),
];
}
La richiesta di autorizzazione invia la challenge S256 del verificatore e l'identificatore della tua API come resource (RFC 8707). Senza resource, l'access token non è destinato alla tua API e l'API lo rifiuta.
return $this->service()->getAuthorizationUri($this->client(), [
// messages:read is a scope of your API; the example API requires it on /api/messages.
'scope' => 'openid profile email messages:read',
'state' => $transaction['state'],
'nonce' => $transaction['nonce'],
// PKCE (RFC 7636): S256 of the verifier that only this session knows.
'code_challenge' => self::base64url(hash('sha256', $transaction['code_verifier'], true)),
'code_challenge_method' => 'S256',
// RFC 8707: an access token for your API.
'resource' => $this->config->apiAudience,
]);
L'accesso è un modulo che invia una POST a /login con il token CSRF, così un altro sito non può avviare un accesso per conto dell'utente:
<form action="/login" method="post">
@csrf
<button type="submit">Sign in</button>
</form>
5. Gestisci il callback
La libreria lascia il controllo del state alla tua app. Il callback prende la transazione dalla sessione, così uno state vale una sola volta, e lo confronta in tempo costante. Il tuo ambiente si identifica in ogni risposta di autorizzazione (RFC 9207), quindi il callback rifiuta anche una risposta il cui iss manca o indica un altro issuer:
$transaction = $request->session()->pull('keenauth.transaction');
$state = $request->query('state');
if (!is_array($transaction) || !is_string($state) || !hash_equals($transaction['state'], $state)) {
return $this->failure('invalid_state');
}
// RFC 9207: the response names its issuer, so a response from another provider is refused.
$iss = $request->query('iss');
if ($keenauth->issuerInResponse() && $iss !== $keenauth->config->issuer) {
return $this->failure('invalid_issuer');
}
Dal callback viene preso solo il codice. La libreria lo scambia con il verificatore PKCE e controlla l'ID token: la firma con le chiavi del tuo ambiente, iss, aud, exp, iat, l'algoritmo e il nonce di questo accesso.
return $this->service()->callback($this->client(), ['code' => $code], $this->config->redirectUri(),
AuthSession::fromArray($transaction));
L'app poi avvia un nuovo id di sessione e conserva l'utente e i token nella sessione lato server:
// A new session id after sign-in prevents session fixation.
$request->session()->regenerate(destroy: true);
Le impostazioni di sessione sono in config/session.php: il driver file, un cookie cifrato, HttpOnly, SameSite=Lax, che è Secure a meno che tu non imposti SESSION_SECURE_COOKIE=false per http://localhost. Con più server, usa il driver redis o database.
6. Proteggi le pagine e chiama la tua API
Le route che richiedono un utente usano il middleware keenauth. L'esempio non ha un refresh token: quando l'access token scade, l'utente accede di nuovo.
Route::get('/dashboard', [DashboardController::class, 'show'])->middleware('keenauth');
La dashboard chiama la tua API con l'access token come bearer token. Il token resta sul server; il browser riceve solo l'HTML.
$response = $http->sendRequest(new HttpRequest('GET', $keenauth->config->apiUrl . '/api/messages', [
'Authorization' => 'Bearer ' . $request->session()->get('keenauth.access_token'),
'Accept' => 'application/json',
]));
7. Disconnetti
La disconnessione è una POST con il token CSRF. L'app termina la sessione locale e manda il browser all'endpoint di logout del tuo ambiente (OpenID Connect RP-Initiated Logout) con id_token_hint, client_id e post_logout_redirect_uri, che riporta il browser alla tua app:
$idToken = $request->session()->get('keenauth.id_token');
$request->session()->invalidate();
$request->session()->regenerateToken();
8. Esegui l'app
Avvia l'API del quickstart Express (o qualsiasi API che accetti i token) ed esegui l'app:
php artisan serve
Apri http://localhost:8000, scegli Accedi, accedi su KeenAuth e apri la dashboard.
9. Testa senza rete
I test non richiedono rete né un account KeenAuth. Un client PSR-18 finto prende il posto del client HTTP e simula il tuo ambiente (discovery, un set di chiavi creato per l'esecuzione dei test, l'endpoint dei token) e l'API. I test girano con APP_ENV=ci, perché Laravel salta il controllo CSRF sotto testing e questi test lo verificano.
public function test_callback_with_an_unknown_state_is_refused_before_any_token_request(): void
{
$this->startSignIn();
$this->returnFromKeenAuth(['state' => 'not-the-state'])->assertStatus(401)->assertSee('invalid_state');
vendor/bin/phpunit
php ../scripts/php_deps.php # exact pins, commits and licences
I test verificano che la configurazione venga validata (e che nessun errore contenga un segreto); che l'accesso richieda il token CSRF e invii PKCE S256, state, nonce e resource; che il codice venga scambiato con il verificatore corrispondente e con l'autenticazione del client; che l'access token venga inviato all'API nell'header Authorization e mai in un cookie; che uno state sconosciuto o riutilizzato, un iss mancante o errato, un ID token per un altro client o con un altro nonce e un errore di autorizzazione vengano rifiutati senza creare una sessione; che una pagina senza sessione reindirizzi alla home page; e che la disconnessione richieda il token CSRF e vada all'endpoint di logout con id_token_hint.
L'accesso completo con un browser reale richiede un ambiente KeenAuth. Non fa parte dei test offline.
Dietro un proxy o un load balancer
Imposta APP_BASE_URL sull'URL pubblico dell'app; l'URI di redirect viene costruito a partire da esso, mai dall'header Host. Se un proxy termina la TLS, configura i proxy fidati di Laravel solo per quel proxy.
Limiti attuali
- Nessun refresh token e nessun accesso silenzioso: l'utente accede di nuovo quando l'access token scade.
- Il client si autentica con un client secret. La libreria supporta anche
private_key_jwt; l'esempio non lo mostra.
Passi successivi
- Proteggi un'API Express: l'API che questa app chiama.
- Verifica dei token: il verificatore sotto il middleware della tua API.