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

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

Passi successivi