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) avec un secret client (
client_secret_basic). Dans un environnementdevelopment, enregistrez l'URI de redirectionhttp://localhost/auth/callbacket l'URI de redirection après déconnexionhttp://localhost/; une URI de boucle locale est enregistrée sans port et correspond à n'importe quel port, par exemple8000. - Une API enregistrée dans l'environnement (Créer une API) avec l'autorisation
messages:read. Sonidentifier, par exemplehttps://api.example.com, est l'audience de ses jetons d'accès. Le guide de démarrage rapide Express crée une API qui accepte ces jetons. - PHP 8.3 ou version ultérieure et Composer 2.
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
- 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 : l'API que cette application appelle.
- Vérification des jetons : le vérificateur sous le middleware de votre API.