
# Ajouter la connexion à une application Laravel

Ce guide de démarrage rapide ajoute la connexion KeenAuth à une application Laravel 13 avec le client OpenID Connect standard `facile-it/php-openid-client`. KeenAuth ne dispose pas encore de SDK PHP, et il n'en faut pas : KeenAuth est un fournisseur OpenID Connect standard, donc une bibliothèque cliente OpenID Connect standard fonctionne. Votre application est le client OAuth et conserve chaque jeton dans la session côté serveur ; le navigateur ne reçoit qu'un cookie d'identifiant de session chiffré. Vous ajoutez les routes de connexion, protégez une page, appelez une API protégée avec le jeton d'accès de l'utilisateur, déconnectez-vous et testez l'application hors ligne.

L'application possède les routes suivantes :

| Route | Ce qu'elle fait |
|---|---|
| `GET /` | Page d'accueil publique : un bouton de connexion, ou le nom de l'utilisateur et un bouton de déconnexion |
| `GET /dashboard` | Nécessite une session. Affiche l'utilisateur et les messages renvoyés par votre API |
| `POST /login` | Démarre la connexion. Laravel vérifie le jeton CSRF |
| `GET /auth/callback` | Vérifie la réponse, échange le code et démarre la session |
| `POST /logout` | Termine la session ici et chez KeenAuth |

## Avant de commencer

- Un environnement KeenAuth. Son émetteur ressemble à `https://your-environment.eu.keenauth.app`.
- Une application web dans l'environnement ([Créer une application](/docs/api/management/applications/create)) avec un secret client (`client_secret_basic`). Dans un environnement `development`, enregistrez l'URI de redirection `http://localhost/auth/callback` et l'URI de redirection après déconnexion `http://localhost/` ; une URI de boucle locale est enregistrée sans port et correspond à n'importe quel port, par exemple `8000`.
- Une API enregistrée dans l'environnement ([Créer une API](/docs/api/management/apis/create)) avec l'autorisation `messages:read`. Son `identifier`, par exemple `https://api.example.com`, est l'audience de ses jetons d'accès. Le [guide de démarrage rapide Express](/docs/quickstarts/express) crée une API qui accepte ces jetons.
- PHP 8.3 ou version ultérieure et Composer 2.

## 1. Installer

```sh
composer require facile-it/php-openid-client:1.0.0 guzzlehttp/guzzle:8.3.1
```

La bibliothèque nécessite un client HTTP PSR-18 ; Guzzle en est un. L'exemple fixe chaque paquet exactement dans `composer.json`, et `composer.lock` fixe chaque paquet à un commit. L'installation se fait avec `composer install --no-plugins --no-scripts`, donc aucun code de paquet ne s'exécute pendant l'installation.

## 2. Configurer

L'application lit sa configuration uniquement depuis des variables d'environnement, via `config/keenauth.php`. Placez-les dans `.env`, que Laravel charge, et ne committez jamais ce fichier.

| Variable | Obligatoire | Signification |
|---|---|---|
| `KEENAUTH_ISSUER` | oui | L'émetteur de votre environnement, en `https` |
| `KEENAUTH_CLIENT_ID` | oui | Le `client_id` de l'application web |
| `KEENAUTH_CLIENT_SECRET` | oui | Son secret client |
| `APP_BASE_URL` | oui | L'URL publique de l'application. L'URI de redirection est construite à partir de celle-ci. `http` uniquement pour `localhost`, `127.0.0.1` ou `[::1]` |
| `API_URL` | oui | L'API que l'application appelle, par exemple `http://localhost:8080` |
| `KEENAUTH_API_AUDIENCE` | oui | L'identifiant de cette API dans KeenAuth |
| `KEENAUTH_ID_TOKEN_ALG` | non | L'algorithme avec lequel votre environnement signe les jetons ID : `RS256` (par défaut), `PS256`, `ES256` ou `EdDSA` |
| `APP_KEY` | oui | La clé propre à Laravel ; elle chiffre le cookie de session. Générez-en une avec `php artisan key:generate` |

`Config::fromArray` vérifie ces valeurs. Une valeur manquante ou inutilisable arrête la première requête qui en a besoin, et aucun message d'erreur ne contient une valeur. Un algorithme HMAC est refusé, de sorte que le secret client ne peut jamais signer un jeton ID :

```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. Créer le client OpenID Connect

`App\KeenAuth\KeenAuth` construit le client à partir de la découverte (`<issuer>/.well-known/openid-configuration`) et met en cache les métadonnées et l'ensemble de clés pendant une heure :

```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,
            ]))
```

Le fournisseur de services l'enregistre comme singleton, et le client HTTP comme liaison distincte, afin que les tests puissent le remplacer.

## 4. Démarrer la connexion

Chaque connexion crée un nouveau `state`, un `nonce` et un vérificateur PKCE, et les conserve dans la session :

```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 requête d'autorisation envoie le challenge S256 du vérificateur et l'identifiant de votre API sous la forme `resource` (RFC 8707). Sans `resource`, le jeton d'accès n'est pas destiné à votre API, et l'API le rejette.

```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,
        ]);
```

La connexion est un formulaire qui envoie une requête POST à `/login` avec le jeton CSRF, afin qu'un autre site ne puisse pas démarrer une connexion à la place de l'utilisateur :

```php
    <form action="/login" method="post">
      @csrf
      <button type="submit">Sign in</button>
    </form>
```

## 5. Traiter le callback

La bibliothèque laisse la vérification du `state` à votre application. Le callback retire la transaction de la session, de sorte qu'un `state` ne fonctionne qu'une fois, et le compare en temps constant. Votre environnement s'identifie dans chaque réponse d'autorisation (RFC 9207) ; le callback refuse donc aussi une réponse dont `iss` est absent ou désigne un autre émetteur :

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

Seul le code est extrait du callback. La bibliothèque l'échange avec le vérificateur PKCE et contrôle le jeton ID : sa signature avec les clés de votre environnement, `iss`, `aud`, `exp`, `iat`, l'algorithme et le `nonce` de cette connexion.

```php
        return $this->service()->callback($this->client(), ['code' => $code], $this->config->redirectUri(),
            AuthSession::fromArray($transaction));
```

L'application démarre ensuite un nouvel identifiant de session et conserve l'utilisateur et les jetons dans la session côté serveur :

```php
        // A new session id after sign-in prevents session fixation.
        $request->session()->regenerate(destroy: true);
```

Les paramètres de session se trouvent dans `config/session.php` : le pilote `file`, et un cookie chiffré, HttpOnly, `SameSite=Lax`, qui est `Secure` sauf si vous définissez `SESSION_SECURE_COOKIE=false` pour `http://localhost`. Avec plusieurs serveurs, utilisez le pilote `redis` ou `database`.

## 6. Protéger les pages et appeler votre API

Les routes qui nécessitent un utilisateur utilisent le middleware `keenauth`. L'exemple n'a pas de jeton de rafraîchissement : lorsque le jeton d'accès expire, l'utilisateur se reconnecte.

```php
Route::get('/dashboard', [DashboardController::class, 'show'])->middleware('keenauth');
```

Le tableau de bord appelle votre API avec le jeton d'accès comme jeton porteur. Le jeton reste sur le serveur ; le navigateur ne reçoit que le 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. Se déconnecter

La déconnexion est une requête `POST` avec le jeton CSRF. L'application termine la session locale et envoie le navigateur vers le point de terminaison de déconnexion de votre environnement (OpenID Connect RP-Initiated Logout) avec `id_token_hint`, `client_id` et `post_logout_redirect_uri`, qui le renvoie vers votre application :

```php
        $idToken = $request->session()->get('keenauth.id_token');
        $request->session()->invalidate();
        $request->session()->regenerateToken();
```

## 8. Exécuter l'application

Démarrez l'API du guide de démarrage rapide Express (ou toute API qui accepte les jetons) et lancez l'application :

```sh
php artisan serve
```

Ouvrez `http://localhost:8000`, choisissez **Se connecter**, connectez-vous chez KeenAuth et ouvrez le tableau de bord.

## 9. Tester sans réseau

Les tests ne nécessitent ni réseau ni compte KeenAuth. Un faux client PSR-18 remplace le client HTTP et joue le rôle de votre environnement (découverte, ensemble de clés créé pour l'exécution des tests, point de terminaison de jeton) ainsi que celui de l'API. Les tests s'exécutent avec `APP_ENV=ci`, car Laravel ignore la vérification CSRF sous `testing` et ces tests la vérifient.

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

Les tests vérifient que la configuration est validée (et qu'aucune erreur ne contient un secret) ; que la connexion exige le jeton CSRF et envoie PKCE S256, `state`, `nonce` et `resource` ; que le code est échangé avec le vérificateur correspondant et l'authentification du client ; que le jeton d'accès est envoyé à l'API dans l'en-tête `Authorization` et n'est jamais placé dans un cookie ; qu'un `state` inconnu ou réutilisé, un `iss` absent ou incorrect, un jeton ID destiné à un autre client ou avec un autre nonce, et une erreur d'autorisation sont refusés sans session ; qu'une page sans session redirige vers la page d'accueil ; et que la déconnexion exige le jeton CSRF et se rend au point de terminaison de déconnexion avec `id_token_hint`.

La connexion complète avec un navigateur réel nécessite un environnement KeenAuth. Elle ne fait pas partie des tests hors ligne.

## Derrière un proxy ou un équilibreur de charge

Définissez `APP_BASE_URL` sur l'URL publique de l'application ; l'URI de redirection est construite à partir de celle-ci, jamais à partir de l'en-tête `Host`. Si un proxy termine le TLS, configurez les proxys de confiance de Laravel pour ce proxy uniquement.

## Limites actuelles

- Pas de jeton de rafraîchissement et pas de connexion silencieuse : l'utilisateur se reconnecte lorsque le jeton d'accès expire.
- Le client s'authentifie avec un secret client. La bibliothèque prend aussi en charge `private_key_jwt` ; l'exemple ne le montre pas.

## Étapes suivantes

- [Protéger une API Express](/docs/quickstarts/express) : l'API que cette application appelle.
- [Vérification des jetons](/docs/sdk/token-verification) : le vérificateur sous le middleware de votre API.
