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

1. Installer

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 :

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

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

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 :

    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.

        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 :

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

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

        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 :

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

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.

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

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

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.

    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

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

Étapes suivantes