
# Go net/http-Sitzungen

Das Paket `github.com/keenauth/keenauth-go/bff` fügt einer Go-`net/http`-Anwendung die Anmeldung hinzu. Ihr Server ist der OAuth-Client und bewahrt alle Token auf dem Server auf. Der Browser erhält nur ein verschlüsseltes HttpOnly-Sitzungs-Cookie und sieht nie ein Access-, Refresh- oder ID-Token. Dies ist das Backend-for-Frontend-(BFF)-Muster. Das Paket verwendet nur die Standardbibliothek. Es ist noch nicht veröffentlicht.

Um stattdessen eine API zu schützen, die Bearer-Token empfängt, siehe [Ihre API schützen](/docs/sdk/protect-api).

## Bevor Sie beginnen

1. Erstellen Sie im Dashboard eine Web-Anwendung in Ihrer Umgebung.
2. Wählen Sie die Client-Authentifizierung: ein Client-Secret (`bff.ClientSecretBasic`) oder `private_key_jwt` (`bff.PrivateKeyJWT`). Bei `private_key_jwt` registrieren Sie den öffentlichen Schlüssel der Anwendung und bewahren den privaten Schlüssel auf Ihrem Server auf.
3. Fügen Sie die Redirect-URI `https://app.acme.example/auth/callback` und die Redirect-URI nach dem Abmelden `https://app.acme.example/` hinzu.
4. Erzeugen Sie ein Cookie-Secret mit mindestens 32 Zeichen, zum Beispiel mit `openssl rand -base64 32`. Bewahren Sie es in Ihrem Secret-Store auf, nicht im Repository.

## Einrichtung

```go
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/keenauth/keenauth-go/bff"
)

func main() {
	auth, err := bff.New(bff.Options{
		Issuer:     "https://acme.eu.keenauth.app", // your environment's issuer, exactly
		ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
		ClientAuth: bff.ClientSecretBasic{Secret: os.Getenv("KEENAUTH_CLIENT_SECRET")},
		AppBaseURL: "https://app.acme.example",
		Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
	})
	if err != nil {
		panic(err) // configuration errors stop the app at start-up
	}

	mux := http.NewServeMux()
	mux.Handle("GET /orders", auth.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s := bff.FromContext(r.Context()).Session
		fmt.Fprintf(w, "Signed in as %s", s.User.Email)
	})))

	http.ListenAndServe(":8080", auth.Middleware(mux))
}
```

Um stattdessen `private_key_jwt` statt eines Client-Secrets zu verwenden, übergeben Sie den privaten Schlüssel. Das Paket signiert für jede Token- und PAR-Anfrage eine neue Client-Assertion (`iss` und `sub` sind Ihre Client-ID, `aud` ist der Issuer, Lebensdauer 2 Minuten, bei jeder Anfrage ein neues `jti`):

```go
pemBytes, err := os.ReadFile("/run/secrets/keenauth-app-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes) // import "github.com/keenauth/keenauth-go/assertion"
if err != nil {
	panic(err)
}
auth, err := bff.New(bff.Options{
	Issuer:     "https://acme.eu.keenauth.app",
	ClientID:   "app_01j9zzzzzzzzzzzzzzzzzzzz",
	ClientAuth: bff.PrivateKeyJWT{Key: key, KeyID: "app-key-2026-11"},
	AppBaseURL: "https://app.acme.example",
	Secrets:    []string{os.Getenv("KEENAUTH_COOKIE_SECRET")},
})
```

ES256 (P-256), EdDSA (Ed25519), RS256 und PS256 (RSA, 2048 Bit oder mehr) werden unterstützt; der Algorithmus richtet sich nach dem Schlüssel, und `Algorithm: "PS256"` wählt PSS für einen RSA-Schlüssel aus. `Key` kann jeder `crypto.Signer` sein, sodass auch ein in einem KMS oder HSM gehaltener Schlüssel funktioniert. Wenn Sie anderswo signieren, setzen Sie stattdessen `bff.PrivateKeyJWT{Assertion: f}`, wobei `f` bei jedem Aufruf eine neu signierte Assertion zurückgibt. Setzen Sie entweder `Key` oder `Assertion`, nicht beides.

`AppBaseURL` ist die öffentliche Adresse Ihrer App. Das Paket erstellt daraus die Redirect-URI und alle Weiterleitungen, niemals aus dem `Host`- oder `X-Forwarded-*`-Header der Anfrage. Unverschlüsseltes `http` wird nur für `localhost` akzeptiert.

## Routen

`Middleware` stellt drei Routen bereit und liest bei jeder anderen Anfrage die Sitzung. Sie können die Routen auch allein einbinden mit `mux.Handle("/auth/", auth)`.

| Route | Beschreibung |
|---|---|
| `GET /auth/login` | Startet die Anmeldung. Optionale Query-Parameter: `returnTo` (wohin der Benutzer gelangt) und `organization` (Anmeldung bei einer Organisation). |
| `GET /auth/callback` | Schließt die Anmeldung ab und setzt das Session-Cookie. |
| `GET` oder `POST /auth/logout` | Meldet bei KeenAuth ab und kehrt zu Ihrer App zurück. |

Die Anmeldung verwendet den Authorization-Code-Flow mit PKCE (S256). Die Anfrage wird an den Server übermittelt (PAR), sofern Ihre Umgebung dies anbietet. Beim Callback prüft das Paket `state`, `iss` und das ID-Token einschließlich seines `nonce`. Jede erfolgreiche Anmeldung erzeugt eine neue Sitzung; die alte wird verworfen.

`returnTo` muss ein Pfad Ihrer eigenen App sein, etwa `/orders?tab=open`. Jeder andere Wert, etwa `//evil.example` oder eine vollständige URL, wird ignoriert, und der Benutzer gelangt auf `/`.

## Handler schützen

`auth.RequireAuth(handler)` lässt eine Anfrage nur mit einer Sitzung durch:

- eine Seitenanfrage (`GET` oder `HEAD`) ohne Sitzung wird mit `returnTo` auf `/auth/login` umgeleitet;
- eine Anfrage an `/api/…` oder mit einer anderen Methode erhält `401` mit einem Problem-Dokument. Übergeben Sie `bff.GuardOptions{API: true}`, um auf jedem Pfad mit `401` zu antworten.

Bei `POST`, `PUT`, `PATCH`, `DELETE` und anderen unsicheren Methoden muss der Browser angeben, dass die Anfrage von Ihrer App stammt: `Sec-Fetch-Site: same-origin` oder ein `Origin`-Header, der dem Ursprung Ihrer App entspricht. Andere Anfragen erhalten `403`. Dies schützt zusätzlich zu `SameSite=Lax` vor Cross-Site-Request-Forgery.

## Sitzung verwenden und Ihre API aufrufen

`bff.FromContext(r.Context())` liefert:

- `Session`: `nil` oder den angemeldeten Benutzer. `Session.User` enthält typisierte ID-Token-Claims wie `Subject`, `Email`, `Name`, `OrgID` und `OrgRoles`; alle Claims stehen in `Session.User.Claims`. Die Sitzung enthält niemals ein Token.
- `AccessToken`: ein gültiges Access-Token für serverseitige Aufrufe oder `""`. Senden Sie es nicht an den Browser.

Läuft das Access-Token innerhalb von 60 Sekunden ab, erneuert das Paket es, bevor Ihr Handler ausgeführt wird. Jede Erneuerung rotiert das Refresh-Token und setzt das neue Session-Cookie in der Antwort. Verweigert der Server das Refresh-Token, endet die Sitzung. Ein vorübergehender Serverfehler lässt die Sitzung unverändert. Gleichzeitige Anfragen in einem Prozess teilen sich eine Erneuerung.

## Organisation wechseln

`auth.SwitchOrganizationPath("org_01j9…", "/dashboard")` gibt einen Link zu `/auth/login` zurück, der den Benutzer erneut in dieser Organisation anmeldet. KeenAuth prüft die Mitgliedschaft. Nach der Anmeldung enthält `Session.User.OrgID` die Organisation.

## Abmelden

Senden Sie aus einem Formular ein `POST` an `/auth/logout`. Das Paket löscht das Session-Cookie (und die gespeicherte Sitzung, falls Sie einen Store verwenden), widerruft das Refresh-Token und leitet den Browser zum Logout-Endpunkt Ihrer Umgebung weiter, der ihn zu Ihrer App zurückbringt. Eine Anfrage, die der Browser als von einer anderen Website stammend kennzeichnet, wird abgelehnt. Browser, die kein `Sec-Fetch-Site` senden, können sich nur mit einem `POST` von Ihren eigenen Seiten abmelden.

## Sitzungsspeicher

Standardmäßig befindet sich die gesamte Sitzung im Cookie, verschlüsselt und mit AES-256-GCM authentifiziert, mit einem aus dem Secret abgeleiteten Schlüssel. Ein verändertes oder kopiertes Cookie wird abgelehnt. Das Cookie heißt `__Host-keenauth_session.0` und wird mit `Secure`, `HttpOnly`, `SameSite=Lax` und `Path=/` gesetzt. Auf `http://localhost` entfallen das Präfix `__Host-` und `Secure`. Große Sitzungen werden auf bis zu drei Cookies aufgeteilt.

Um Token auf dem Server zu halten, setzen Sie `Options.Store` auf ein `bff.SessionStore` (`Get`, `Set`, `Delete`), zum Beispiel mit Redis als Backend. Das Cookie enthält dann nur eine verschlüsselte zufällige Sitzungs-ID. Jede Anmeldung erzeugt eine neue ID und löscht den vorherigen Datensatz. Der Store enthält Token, daher beschränken Sie den Zugriff darauf. `bff.MemoryStore` ist nur für Entwicklung und Tests gedacht.

Eine Sitzung dauert höchstens 7 Tage (`SessionMaxAge`); eine Erneuerung verlängert sie nicht. Um das Secret zu ändern, tragen Sie das neue zuerst ein: `Secrets: []string{newSecret, oldSecret}`. Entfernen Sie das alte nach `SessionMaxAge`.

## Aktuelle Einschränkungen

- DPoP-gebundene Token und Back-Channel-Logout werden noch nicht unterstützt.
- Erneuerungen werden nur innerhalb eines Serverprozesses geteilt. Bei mehreren Instanzen lässt die Anwendungseinstellung `refresh_reuse_interval_s` (Standard 5 Sekunden) eine parallele Erneuerung erfolgreich sein; setzen Sie sie auf `0`, beendet die zweite Erneuerung die Sitzung.
