
# Go-Server-SDK

Das Modul `github.com/keenauth/keenauth-go` ist der Go-Client der KeenAuth-Management-API. Typisierte Methoden für jede `/v1`-Ressource werden aus dem OpenAPI-Dokument generiert (Paket `management`, eingebettet in `keenauth.Client`). Der handgeschriebene Kern ergänzt Authentifizierung, Wiederholungen, Idempotenzschlüssel, typisierte Fehler und Warnungen zur Veraltung. Das Modul verwendet nur die Standardbibliothek. Es ist noch nicht veröffentlicht.

Zur Anmeldung in einer `net/http`-Anwendung siehe [Go net/http-Sitzungen](/docs/sdk/go-nethttp). Zum Schutz Ihrer eigenen API siehe [Schutz Ihrer API](/docs/sdk/protect-api).

## Client mit einem API-Schlüssel erstellen

```go
import (
	"context"
	"fmt"
	"os"

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

c, err := keenauth.NewClient("https://api.eu.keenauth.com/v1", os.Getenv("KEENAUTH_API_KEY"))
if err != nil {
	panic(err)
}
user, _, err := c.UsersAPI.GetUser(context.Background(), "usr_01j9zzzzzzzzzzzzzzzzzzzz").Execute()
if err != nil {
	panic(err)
}
fmt.Println(user.GetEmail())
```

Die Basis-URL ist die API-Adresse Ihrer Region gefolgt von `/v1`. Der Schlüssel (`idp_sk_…` oder `idp_rk_…`) wird nur im `Authorization`-Header und nur an diese Adresse gesendet; eine Weiterleitung auf einen anderen Host oder auf unverschlüsseltes `http` übermittelt ihn nie. Unverschlüsseltes `http` wird nur für `localhost` akzeptiert.

Optionen:

- `keenauth.WithHTTPClient(h)`: Ihr eigener `http.Client` (sein Transport wird umhüllt, nicht verändert).
- `keenauth.WithTimeout(d)`: Gesamtzeitlimit eines Aufrufs, einschließlich Wiederholungen (Standard 30 Sekunden).
- `keenauth.WithClientName("my-tool", "1.4.0")`: fügt Ihr Werkzeug zu `User-Agent` und `Idp-Client` hinzu.
- `keenauth.WithLogger(l)`: der `*slog.Logger` für SDK-Warnungen (Standard `slog.Default()`).

## Mit private_key_jwt authentifizieren

Statt eines statischen Schlüssels kann sich eine Management-Anwendung mit einem eigenen Schlüsselpaar anmelden. Das SDK holt über den Token-Endpunkt Ihrer Umgebung ein Management-Token, hält es im Speicher und ersetzt es 60 Sekunden vor Ablauf. Gleichzeitige Aufrufe teilen sich eine Token-Anfrage.

1. Erstellen Sie im Dashboard eine Management-Anwendung und registrieren Sie ihren öffentlichen Schlüssel (als JWKS-URL oder direkt; bis zu 3 aktive Schlüssel).
2. Bewahren Sie den privaten Schlüssel auf Ihrem Server auf, zum Beispiel in einer Geheimdatei oder einem KMS.

```go
import (
	"os"

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

pemBytes, err := os.ReadFile("/run/secrets/keenauth-mgmt-key.pem")
if err != nil {
	panic(err)
}
key, err := assertion.ParsePrivateKeyPEM(pemBytes)
if err != nil {
	panic(err)
}
c, err := keenauth.NewClientWithPrivateKeyJWT("https://api.eu.keenauth.com/v1", keenauth.PrivateKeyJWT{
	Issuer:   "https://acme.eu.keenauth.app", // your environment's issuer, exactly
	ClientID: "app_01j9zzzzzzzzzzzzzzzzzzzz",
	Key:      key,
	KeyID:    "mgmt-key-2026-11", // the kid you registered; optional with a single key
	Scope:    "users:read users:write", // optional; default: the grant's default scopes
})
```

Was das SDK sendet:

- `POST <Issuer>/oauth2/token` mit `grant_type=client_credentials`, `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`, einer neuen `client_assertion` und `resource` mit der API-Adresse (`https://api.eu.keenauth.com`; ändern mit `Audience`).
- Die Assertion ist ein JWT mit `iss` und `sub` gleich Ihrer Client-ID, `aud` gleich dem Issuer, einer zufälligen `jti` und einer Lebensdauer von 2 Minuten. Sie wird mit ES256 (P-256-Schlüssel), EdDSA (Ed25519), RS256 oder PS256 (RSA, 2048 Bit oder mehr; `Algorithm: "PS256"` für PSS) signiert.

`Key` ist jeder `crypto.Signer`, sodass ein im KMS oder HSM gehaltener Schlüssel ohne Export funktioniert. Das Management-Token ist 10 Minuten gültig. Antwortet die API mit `401`, verwirft das SDK das zwischengespeicherte Token, und der nächste Aufruf erhält ein neues.

Eine abgelehnte Token-Anfrage liefert einen `*keenauth.TokenError` mit HTTP-Status und dem OAuth-`error`-Code, zum Beispiel `invalid_client` bei einem unbekannten Schlüssel oder einer falschen `KeyID`. Er enthält nie die Assertion oder ein Token:

```go
var te *keenauth.TokenError
if errors.As(err, &te) {
	log.Printf("token request refused: %d %s", te.Status, te.Code)
}
```

## Wiederholungen und Idempotenzschlüssel

Das SDK wiederholt einen Aufruf höchstens 3 Mal insgesamt, und nur wenn beide Bedingungen zutreffen:

- die Antwort ist `429`, `502`, `503` oder `504`, oder die Verbindung ist fehlgeschlagen (ein Netzwerkfehler, kein Zertifikatsfehler);
- die Methode ist `GET`, `PUT` oder `DELETE`, oder ein `POST` mit einem `Idempotency-Key`.

Jede `POST`-Operation, die einen `Idempotency-Key` akzeptiert (alle außer den nebenwirkungsfreien Aufrufen `users:search` und `…:preview` sowie `users/{id}:unlock`), erhält einen neuen UUIDv4-Schlüssel, und jede Wiederholung dieses Aufrufs sendet denselben Schlüssel und denselben Body, sodass die Operation höchstens einmal ausgeführt wird. Um einen Aufruf über eigene Neustarts hinweg idempotent zu machen, übergeben Sie Ihren eigenen Schlüssel (16 bis 64 Zeichen aus `A-Z a-z 0-9 _ -`), zum Beispiel `.IdempotencyKey(orderKey)`; das SDK ersetzt ihn nie. Ein `PATCH` mit `If-Match` (jede versionierte Aktualisierung) wird nur bei `429` wiederholt: Der Rate Limiter lehnt eine Anfrage ab, bevor sie läuft, und `If-Match` verhindert, dass eine Änderung zweimal angewendet wird. Jeder andere `PATCH` wird nie wiederholt.

Zwischen den Versuchen wartet das SDK so lange, wie `Retry-After` angibt (Sekunden oder HTTP-Datum). Ohne diesen Header wartet es 0,5 Sekunden, dann 1 Sekunde, jeweils mit ±25 % Jitter. Verlangt `Retry-After` mehr als 60 Sekunden, oder würde das Warten die Deadline Ihres Kontexts überschreiten, gibt das SDK die Antwort zurück, statt zu warten. Das Abbrechen des Kontexts beendet das Warten.

## Über eine Liste iterieren

Listenmethoden liefern eine Seite: `data` und `list_metadata` mit den Cursorn `before` und `after`. `keenauth.All` folgt `after` und liefert jeden Eintrag. Die nächste Seite wird erst geladen, wenn Sie sie erreichen, sodass ein Abbruch der Schleife weitere Anfragen stoppt. Seitengröße und Filter setzen Sie wie gewohnt auf der Anfrage:

```go
for u, err := range keenauth.All(c.UsersAPI.ListUsers(ctx).Limit(100).Status("active")) {
	if err != nil {
		return err // the iteration ends after an error
	}
	fmt.Println(u.GetId(), u.GetEmail())
}
```

`keenauth.AllBefore` folgt stattdessen `before`, zum Beispiel zu neueren Einträgen von `ListLogs`. Jede generierte Listenmethode funktioniert mit beiden, außer `ListEvents` der Events-API, die mit `next_cursor` und `has_more` paginiert. Eine Range-over-Func-Schleife benötigt Go 1.23 oder neuer.

## Webhooks verifizieren

Das Paket `github.com/keenauth/keenauth-go/webhook` prüft die Signatur einer Webhook-Zustellung (Standard Webhooks). Übergeben Sie ihm das Signing-Secret des Endpunkts (`idp_whsec_…`) und den rohen Body genau so, wie er empfangen wurde:

```go
import (
	"io"
	"net/http"
	"os"

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

verifier, err := webhook.NewVerifier([]string{os.Getenv("KEENAUTH_WEBHOOK_SECRET")})
if err != nil {
	panic(err) // webhook.ErrSecret: the secret is not well formed
}

http.HandleFunc("POST /webhooks/keenauth", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
	if err != nil {
		http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
		return
	}
	if err := verifier.Verify(r.Header, body); err != nil {
		http.Error(w, "invalid webhook", http.StatusBadRequest)
		return
	}
	// Handle the event. Store webhook-id and skip a delivery you have already processed.
	w.WriteHeader(http.StatusNoContent)
})
```

`Verify` akzeptiert eine Zustellung, wenn `webhook-timestamp` innerhalb von 5 Minuten zur aktuellen Zeit liegt (in beide Richtungen) und eine der `v1`-Signaturen in `webhook-signature` übereinstimmt. Signaturen werden in konstanter Zeit verglichen, andere Signaturversionen werden ignoriert. Der Fehler sagt Ihnen, warum eine Zustellung abgelehnt wurde; jeder entspricht einem von `webhook.ErrHeaders` (ein Header fehlt), `webhook.ErrTimestamp`, `webhook.ErrSignature` oder `webhook.ErrSecret`, prüfbar mit `errors.Is`.

Wenn Sie das Secret rotieren, übergeben Sie beide Secrets, neues und altes, bis das alte nicht mehr verwendet wird: `webhook.NewVerifier([]string{newSecret, oldSecret})`. Während der Rotation kann KeenAuth auch zwei Signaturen in einem Header senden; eine davon genügt.

## Typisierte Ereignisse und Claims

Das Paket `github.com/keenauth/keenauth-go/events` enthält je eine Struktur pro Kundenereignistyp, generiert aus den öffentlichen Ereignisschemas. `webhook.Verifier.VerifyEvent` verifiziert eine Zustellung und parst sie; `events.Parse` parst einen bereits verifizierten Body. Verwenden Sie einen Type-Switch, um die Felder eines Typs zu erhalten:

```go
ev, err := verifier.VerifyEvent(r.Header, body)
if err != nil {
	http.Error(w, "invalid webhook", http.StatusBadRequest) // webhook.Err… or events.ErrParse
	return
}
switch e := ev.(type) {
case *events.UserCreated:
	log.Printf("user %s created", e.Data.Object.ID)
case *events.EmailSendingSuspended:
	log.Printf("sending suspended: %s", e.Data.Object.Reason)
case *events.Unknown:
	// A type this SDK version does not know yet. e.Type and e.Raw() are still available.
}
```

- `Parse` prüft den Envelope (`id`, `type`, `schema_version`, `environment_id`, `occurred_at`, `recorded_at`, `seq`, `data`, `context`) und die Member, die das Schema des Typs deklariert. Ein fehlendes oder falsch typisiertes Member liefert einen `*events.ParseError` mit seinem Pfad, etwa `data.object.reason`.
- Ein unbekannter Typ oder ein bekannter Typ mit einer `schema_version`, die dieses SDK nicht modelliert, wird als `*events.Unknown` zurückgegeben. Das ist kein Fehler.
- Member, die das Schema nicht deklariert, bleiben erhalten: `Raw()` liefert das gesamte Ereignis und `ObjectJSON()` liefert `data.object` (im Modus für vollständige Nutzdaten die Darstellung der Ressource aus der Management-API).
- Werte aufgezählter Felder werden nicht gegen die bekannten Werte geprüft, sodass ein neuer Wert vom Server das Parsen nicht unterbricht.

Das Paket `github.com/keenauth/keenauth-go/claims` typisiert die Claims eines verifizierten Tokens: `claims.ParseIDToken` für ein ID-Token (OpenID Connect Core) und `claims.ParseAccessToken` für ein Access-Token (RFC 9068). Sie prüfen die Claim-Typen, verifizieren aber nichts; rufen Sie sie daher auf der `Claims`-Map auf, die das Paket `verify` zurückgibt:

```go
at, err := v.VerifyAccessToken(ctx, token, nil)
if err != nil {
	return err
}
c, err := claims.ParseAccessToken(at.Claims)
if err != nil {
	return err
}
log.Printf("client %s, org %s, roles %v", c.ClientID, c.OrgID, c.OrgRoles)
_ = c.Custom["tenant_tier"] // your own claims
```

`aud` ist immer ein Slice (`claims.Audience`). Claims, die nicht Teil des Vertrags sind, sind Ihre eigenen Claims und werden in `Custom` behalten.

## Fehler

Ein API-Fehler ist ein Problem-Dokument nach RFC 9457. `keenauth.AsProblem(err)` gibt es zusammen mit dem maschinenlesbaren `Code` zurück. Ein unbekannter Code wird unverändert zurückgegeben, sodass neue Server-Codes Ihren Client nicht brechen:

```go
if _, _, err := c.UsersAPI.GetUser(ctx, id).Execute(); err != nil {
	if pe, ok := keenauth.AsProblem(err); ok && pe.Code == "not_found" {
		// …
	}
}
```

## Warnungen zur Veraltung

Trägt eine Antwort einen `Deprecation`-Header (RFC 9745) oder `Sunset`-Header (RFC 8594), protokolliert das SDK pro Operation und Header-Wert einmal für den gesamten Prozess eine Warnung auf Stufe `WARN` im Logger, den Sie mit `WithLogger` setzen. Die Warnung nennt die Methode und den Pfad, wobei Bezeichner durch `{id}` ersetzt sind, die Header-Werte und jeden `Link` mit `rel="deprecation"` oder `rel="sunset"`. Sie enthält nie IDs, Tokens oder Schlüssel, und der Aufruf selbst ist nicht betroffen.

```text
level=WARN msg="keenauth: the API operation is deprecated" operation="GET /users/{id}" deprecation=@1767225600 sunset="Wed, 01 Jul 2026 00:00:00 GMT"
```
