Anmeldung in eine Laravel-Anwendung einbauen

Dieser Schnellstart fügt der Laravel-13-Anwendung die Anmeldung mit KeenAuth hinzu, und zwar mit dem Standard-OpenID-Connect-Client facile-it/php-openid-client. KeenAuth hat noch kein PHP-SDK, und es wird auch keines benötigt: KeenAuth ist ein Standard-OpenID-Connect-Provider, daher funktioniert eine Standard-OpenID-Connect-Client-Bibliothek. Ihre Anwendung ist der OAuth-Client und bewahrt alle Token in der serverseitigen Sitzung auf; der Browser erhält nur ein verschlüsseltes Sitzungs-ID-Cookie. Sie fügen die Anmelde-Routen hinzu, schützen eine Seite, rufen eine geschützte API mit dem Zugriffstoken des Benutzers auf, melden sich ab und testen die Anwendung offline.

Die Anwendung hat diese Routen:

Route Was sie macht
GET / Öffentliche Startseite: eine Schaltfläche zum Anmelden oder der Name des Benutzers und eine Schaltfläche zum Abmelden
GET /dashboard Benötigt eine Sitzung. Zeigt den Benutzer und die Nachrichten, die Ihre API zurückgibt
POST /login Startet die Anmeldung. Laravel prüft das CSRF-Token
GET /auth/callback Prüft die Antwort, tauscht den Code aus und startet die Sitzung
POST /logout Beendet die Sitzung hier und bei KeenAuth

Bevor Sie beginnen

1. Installieren

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

Die Bibliothek benötigt einen PSR-18-HTTP-Client; Guzzle ist einer davon. Das Beispiel fixiert jedes Paket genau in composer.json, und composer.lock fixiert jedes Paket auf einen Commit. Die Installation erfolgt mit composer install --no-plugins --no-scripts, daher läuft während der Installation kein Paketcode.

2. Konfigurieren

Die Anwendung liest ihre Konfiguration ausschließlich aus Umgebungsvariablen, und zwar über config/keenauth.php. Legen Sie diese in .env ab, das Laravel lädt, und committen Sie diese Datei niemals.

Variable Erforderlich Bedeutung
KEENAUTH_ISSUER ja Der Issuer Ihrer Umgebung, https
KEENAUTH_CLIENT_ID ja Die client_id der Webanwendung
KEENAUTH_CLIENT_SECRET ja Ihr Client-Secret
APP_BASE_URL ja Die öffentliche URL der Anwendung. Die Redirect-URI wird daraus gebildet. http nur für localhost, 127.0.0.1 oder [::1]
API_URL ja Die API, die die Anwendung aufruft, zum Beispiel http://localhost:8080
KEENAUTH_API_AUDIENCE ja Der Bezeichner dieser API in KeenAuth
KEENAUTH_ID_TOKEN_ALG nein Der Algorithmus, mit dem Ihre Umgebung ID-Token signiert: RS256 (Standard), PS256, ES256 oder EdDSA
APP_KEY ja Der eigene Schlüssel von Laravel; er verschlüsselt das Sitzungs-Cookie. Erzeugen Sie ihn mit php artisan key:generate

Config::fromArray prüft diese Werte. Ein fehlender oder unbrauchbarer Wert stoppt die erste Anfrage, die ihn benötigt, und keine Fehlermeldung enthält einen Wert. Ein HMAC-Algorithmus wird abgelehnt, damit das Client-Secret niemals ein ID-Token signieren kann:

        $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. OpenID-Connect-Client erstellen

App\KeenAuth\KeenAuth baut den Client aus der Discovery (<issuer>/.well-known/openid-configuration) und speichert die Metadaten und den Schlüsselsatz eine Stunde lang zwischen:

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

Der Service-Provider registriert ihn als Singleton, den HTTP-Client als eigene Bindung, damit die Tests ihn ersetzen können.

4. Anmeldung starten

Jede Anmeldung erzeugt einen neuen state, nonce und PKCE-Verifier und hält sie in der Sitzung vor:

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

Die Autorisierungsanfrage sendet die S256-Challenge des Verifiers und den Bezeichner Ihrer API als resource (RFC 8707). Ohne resource ist das Zugriffstoken nicht für Ihre API bestimmt, und die API weist es ab.

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

Die Anmeldung ist ein Formular, das mit dem CSRF-Token an /login sendet, damit keine andere Website eine Anmeldung für den Benutzer starten kann:

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

5. Callback verarbeiten

Die Bibliothek überlässt die state-Prüfung Ihrer Anwendung. Der Callback entnimmt die Transaktion der Sitzung, sodass ein state nur einmal funktioniert, und vergleicht ihn in konstanter Zeit. Ihre Umgebung nennt sich in jeder Autorisierungsantwort selbst (RFC 9207). Daher lehnt der Callback auch eine Antwort ab, deren iss fehlt oder einen anderen Issuer nennt:

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

Aus dem Callback wird nur der Code übernommen. Die Bibliothek tauscht ihn mit dem PKCE-Verifier ein und prüft das ID-Token: seine Signatur mit den Schlüsseln Ihrer Umgebung, iss, aud, exp, iat, den Algorithmus und das nonce dieser Anmeldung.

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

Danach vergibt die Anwendung eine neue Sitzungs-ID und hält den Benutzer und die Token in der serverseitigen Sitzung:

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

Die Sitzungseinstellungen stehen in config/session.php: der file-Treiber und ein verschlüsseltes, HttpOnly-Cookie, das SameSite=Lax ist, es sei denn, Sie setzen Secure für SESSION_SECURE_COOKIE=false bzw. http://localhost. Bei mehreren Servern verwenden Sie den Treiber redis oder database.

6. Seiten schützen und Ihre API aufrufen

Routen, die einen Benutzer benötigen, verwenden die Middleware keenauth. Das Beispiel hat kein Refresh-Token: Wenn das Zugriffstoken abläuft, meldet sich der Benutzer erneut an.

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

Das Dashboard ruft Ihre API mit dem Zugriffstoken als Bearer-Token auf. Das Token bleibt auf dem Server; der Browser erhält nur das HTML.

        $response = $http->sendRequest(new HttpRequest('GET', $keenauth->config->apiUrl . '/api/messages', [
            'Authorization' => 'Bearer ' . $request->session()->get('keenauth.access_token'),
            'Accept' => 'application/json',
        ]));

7. Abmelden

Die Abmeldung ist ein POST mit dem CSRF-Token. Die Anwendung beendet die lokale Sitzung und schickt den Browser an den Logout-Endpunkt Ihrer Umgebung (OpenID Connect RP-Initiated Logout) mit id_token_hint, client_id und post_logout_redirect_uri, wodurch er zu Ihrer Anwendung zurückkehrt:

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

8. Anwendung starten

Starten Sie die API aus dem Express-Schnellstart (oder eine andere API, die diese Token akzeptiert) und starten Sie die Anwendung:

php artisan serve

Öffnen Sie http://localhost:8000, wählen Sie Anmelden, melden Sie sich bei KeenAuth an und öffnen Sie das Dashboard.

9. Ohne Netzwerk testen

Die Tests benötigen kein Netzwerk und kein KeenAuth-Konto. Ein gefälschter PSR-18-Client ersetzt den HTTP-Client und übernimmt die Rolle Ihrer Umgebung (Discovery, ein für den Testlauf erzeugter Schlüsselsatz, der Token-Endpunkt) sowie der API. Die Tests laufen mit APP_ENV=ci, weil Laravel die CSRF-Prüfung unter testing überspringt und diese Tests sie prüfen.

    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

Die Tests prüfen, dass die Konfiguration validiert wird (und dass keine Fehlermeldung ein Secret enthält); dass die Anmeldung das CSRF-Token benötigt und PKCE S256 sendet, state, nonce und resource; dass der Code mit dem passenden Verifier und mit Client-Authentifizierung eingetauscht wird; dass das Zugriffstoken im Header Authorization an die API geht und niemals in ein Cookie gelangt; dass ein unbekannter oder wiederverwendeter state, ein fehlender oder falscher iss, ein ID-Token für einen anderen Client oder mit einem anderen Nonce sowie ein Autorisierungsfehler ohne Sitzung abgelehnt werden; dass eine Seite ohne Sitzung auf die Startseite weiterleitet; und dass die Abmeldung das CSRF-Token benötigt und mit id_token_hint an den Logout-Endpunkt geht.

Die vollständige Anmeldung mit einem echten Browser benötigt eine KeenAuth-Umgebung. Sie ist nicht Teil der Offline-Tests.

Hinter einem Proxy oder Load Balancer

Setzen Sie APP_BASE_URL auf die öffentliche URL der Anwendung; die Redirect-URI wird daraus gebildet, niemals aus dem Host-Header. Beendet ein Proxy TLS, konfigurieren Sie die vertrauenswürdigen Proxys von Laravel nur für diesen Proxy.

Aktuelle Einschränkungen

Nächste Schritte