
# 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

- Eine KeenAuth-Umgebung. Ihr Issuer sieht aus wie `https://your-environment.eu.keenauth.app`.
- Eine Webanwendung in der Umgebung ([Anwendung anlegen](/docs/api/management/applications/create)) mit einem Client-Secret (`client_secret_basic`). Registrieren Sie in einer `development`-Umgebung die Redirect-URI `http://localhost/auth/callback` und die Post-Logout-Redirect-URI `http://localhost/`; eine Loopback-URI wird ohne Port registriert und passt auf jeden Port, etwa `8000`.
- Eine API, die in der Umgebung registriert ist ([API anlegen](/docs/api/management/apis/create)), mit dem Scope `messages:read`. Ihre `identifier`, zum Beispiel `https://api.example.com`, ist die Audience ihrer Zugriffstoken. Der [Express-Schnellstart](/docs/quickstarts/express) erstellt eine API, die diese Token akzeptiert.
- PHP 8.3 oder neuer und Composer 2.

## 1. Installieren

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

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

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

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:

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

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.

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

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:

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

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

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.

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

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

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

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

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

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

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

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

- Kein Refresh-Token und keine stille Anmeldung: Der Benutzer meldet sich erneut an, wenn das Zugriffstoken abläuft.
- Der Client authentifiziert sich mit einem Client-Secret. Die Bibliothek unterstützt außerdem `private_key_jwt`; das Beispiel zeigt dies nicht.

## Nächste Schritte

- [Eine Express-API schützen](/docs/quickstarts/express): die API, die diese Anwendung aufruft.
- [Token prüfen](/docs/sdk/token-verification): der Verifier unter der Middleware Ihrer API.
