
# 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](/docs/api/management/applications/create)) con un client secret (`client_secret_basic`). In un ambiente `development`, registra l'URI di redirect `http://localhost/auth/callback` e l'URI di redirect dopo la disconnessione `http://localhost/`; un URI loopback è registrato senza porta e corrisponde a qualsiasi porta, come `8000`.
- Un'API registrata nell'ambiente ([Crea un'API](/docs/api/management/apis/create)) con lo scope `messages:read`. Il suo `identifier`, per esempio `https://api.example.com`, è l'audience dei suoi access token. Il [quickstart Express](/docs/quickstarts/express) crea un'API che accetta questi token.
- PHP 8.3 o successivo e Composer 2.

## 1. Installa

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

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

```php
        $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);
```

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

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

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

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

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

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

```php
        // 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.

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

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

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

```sh
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.

```php
    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');
```

```sh
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](/docs/quickstarts/express): l'API che questa app chiama.
- [Verifica dei token](/docs/sdk/token-verification): il verificatore sotto il middleware della tua API.
