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. Zum Schutz Ihrer eigenen API siehe Schutz Ihrer API.
Client mit einem API-Schlüssel erstellen
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 eigenerhttp.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 zuUser-AgentundIdp-Clienthinzu.keenauth.WithLogger(l): der*slog.Loggerfür SDK-Warnungen (Standardslog.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.
- 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).
- Bewahren Sie den privaten Schlüssel auf Ihrem Server auf, zum Beispiel in einer Geheimdatei oder einem KMS.
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/tokenmitgrant_type=client_credentials,client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer, einer neuenclient_assertionundresourcemit der API-Adresse (https://api.eu.keenauth.com; ändern mitAudience).- Die Assertion ist ein JWT mit
issundsubgleich Ihrer Client-ID,audgleich dem Issuer, einer zufälligenjtiund 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:
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,503oder504, oder die Verbindung ist fehlgeschlagen (ein Netzwerkfehler, kein Zertifikatsfehler); - die Methode ist
GET,PUToderDELETE, oder einPOSTmit einemIdempotency-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:
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:
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:
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.
}
Parseprü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.ParseErrormit seinem Pfad, etwadata.object.reason.- Ein unbekannter Typ oder ein bekannter Typ mit einer
schema_version, die dieses SDK nicht modelliert, wird als*events.Unknownzurückgegeben. Das ist kein Fehler. - Member, die das Schema nicht deklariert, bleiben erhalten:
Raw()liefert das gesamte Ereignis undObjectJSON()liefertdata.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:
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:
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.
level=WARN msg="keenauth: the API operation is deprecated" operation="GET /users/{id}" deprecation=@1767225600 sunset="Wed, 01 Jul 2026 00:00:00 GMT"