SDK serveur Go
Le module github.com/keenauth/keenauth-go est le client Go de l'API de gestion de KeenAuth. Les méthodes typées pour chaque ressource /v1 sont générées à partir du document OpenAPI (package management, intégré dans keenauth.Client). Le cœur écrit à la main ajoute l'authentification, les nouvelles tentatives, les clés d'idempotence, les erreurs typées et les avertissements de dépréciation. Le module n'utilise que la bibliothèque standard. Il n'est pas encore publié.
Pour la connexion dans une application net/http, voir Sessions Go net/http. Pour protéger votre propre API, voir Protéger votre API.
Créer un client avec une clé d'API
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())
L'URL de base est l'adresse de l'API de votre région suivie de /v1. La clé (idp_sk_… ou idp_rk_…) n'est envoyée que dans l'en-tête Authorization et uniquement à cette adresse ; une redirection vers un autre hôte ou vers du http simple ne la transmet jamais. Le http simple n'est accepté que pour localhost.
Options :
keenauth.WithHTTPClient(h): votre proprehttp.Client(son transport est encapsulé, pas modifié).keenauth.WithTimeout(d): durée totale d'un appel, nouvelles tentatives comprises (30 secondes par défaut).keenauth.WithClientName("my-tool", "1.4.0"): ajoute votre outil àUser-AgentetIdp-Client.keenauth.WithLogger(l): le*slog.Loggerpour les avertissements du SDK (slog.Default()par défaut).
S'authentifier avec private_key_jwt
Au lieu d'une clé statique, une application de gestion peut se connecter avec sa propre paire de clés. Le SDK obtient un jeton de gestion auprès du point de terminaison de jetons de votre environnement, le conserve en mémoire et le remplace 60 secondes avant son expiration. Les appels simultanés partagent une seule demande de jeton.
- Dans le tableau de bord, créez une application de gestion et enregistrez sa clé publique (sous forme d'URL JWKS ou directement ; jusqu'à 3 clés actives).
- Conservez la clé privée sur votre serveur, par exemple dans un fichier secret ou un 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
})
Ce que le SDK envoie :
POST <Issuer>/oauth2/tokenavecgrant_type=client_credentials,client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer, une nouvelleclient_assertionetresourcedéfini sur l'adresse de l'API (https://api.eu.keenauth.com; modifiable avecAudience).- L'assertion est un JWT dont
issetsubvalent votre identifiant client,audvaut l'émetteur,jtiest aléatoire et la durée de vie est de 2 minutes. Elle est signée avec ES256 (clé P-256), EdDSA (Ed25519), RS256 ou PS256 (RSA, 2048 bits ou plus ; définissezAlgorithm: "PS256"pour PSS).
Key est n'importe quel crypto.Signer, donc une clé détenue dans un KMS ou un HSM fonctionne sans être exportée. Le jeton de gestion a une durée de vie de 10 minutes. Si l'API répond 401, le SDK supprime le jeton en cache et l'appel suivant en obtient un nouveau.
Un refus de demande de jeton renvoie un *keenauth.TokenError avec le statut HTTP et le code error OAuth, par exemple invalid_client pour une clé inconnue ou un KeyID incorrect. Il ne contient jamais l'assertion ni de jeton :
var te *keenauth.TokenError
if errors.As(err, &te) {
log.Printf("token request refused: %d %s", te.Status, te.Code)
}
Nouvelles tentatives et clés d'idempotence
Le SDK ne relance un appel qu'au maximum 3 fois au total, et seulement si les deux conditions suivantes sont vraies :
- la réponse est
429,502,503ou504, ou la connexion a échoué (erreur réseau, pas erreur de certificat) ; - la méthode est
GET,PUTouDELETE, ou unPOSTavec unIdempotency-Key.
Chaque opération POST qui accepte un Idempotency-Key (toutes sauf les appels sans effet de bord users:search et …:preview et users/{id}:unlock) reçoit une nouvelle clé UUIDv4, et chaque nouvelle tentative de cet appel envoie la même clé et le même corps, de sorte que l'opération ne s'exécute au plus qu'une fois. Pour rendre un appel idempotent même après vos propres redémarrages, passez votre propre clé (16 à 64 caractères parmi A-Z a-z 0-9 _ -), par exemple .IdempotencyKey(orderKey) ; le SDK ne la remplace jamais. Un PATCH qui porte If-Match (toute mise à jour versionnée) n'est relancé que sur 429 : le limiteur de débit refuse une requête avant son exécution, et If-Match empêche qu'une modification soit appliquée deux fois. Tout autre PATCH n'est jamais relancé.
Entre deux tentatives, le SDK attend le délai indiqué par Retry-After (secondes ou date HTTP). Sans cet en-tête, il attend 0,5 seconde, puis 1 seconde, chacune avec une gigue de ±25 %. Si Retry-After demande plus de 60 secondes, ou si l'attente dépasserait l'échéance de votre contexte, le SDK renvoie la réponse au lieu d'attendre. L'annulation du contexte interrompt l'attente.
Parcourir une liste
Les méthodes de liste renvoient une seule page : data et list_metadata avec les curseurs before et after. keenauth.All suit after et produit chaque élément. Il ne récupère la page suivante que lorsque vous l'atteignez, donc sortir de la boucle arrête les requêtes suivantes. Définissez la taille de page et les filtres sur la requête comme d'habitude :
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 suit before à la place, par exemple vers les entrées plus récentes de ListLogs. Toutes les méthodes de liste générées fonctionnent avec les deux, sauf ListEvents de l'API Events, qui pagine avec next_cursor et has_more. Une boucle range-over-func nécessite Go 1.23 ou ultérieur.
Vérifier les webhooks
Le package github.com/keenauth/keenauth-go/webhook vérifie la signature d'une livraison de webhook (Standard Webhooks). Fournissez-lui le secret de signature du point de terminaison (idp_whsec_…) et transmettez le corps brut exactement tel que reçu :
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 accepte une livraison lorsque webhook-timestamp se situe à moins de 5 minutes de l'heure actuelle (dans les deux sens) et qu'une des signatures v1 de webhook-signature correspond. Les signatures sont comparées en temps constant, et les autres versions de signature sont ignorées. L'erreur indique pourquoi une livraison a été refusée ; chacune correspond à l'un de webhook.ErrHeaders (un en-tête manque), webhook.ErrTimestamp, webhook.ErrSignature ou webhook.ErrSecret, à tester avec errors.Is.
Lorsque vous faites tourner le secret, transmettez les deux secrets, le nouveau et l'ancien, jusqu'à ce que l'ancien ne soit plus utilisé : webhook.NewVerifier([]string{newSecret, oldSecret}). Pendant la rotation, KeenAuth peut aussi envoyer deux signatures dans un même en-tête ; l'une ou l'autre suffit.
Événements typés et revendications
Le package github.com/keenauth/keenauth-go/events contient une structure par type d'événement client, générée à partir des schémas publics des événements. webhook.Verifier.VerifyEvent vérifie une livraison et l'analyse ; events.Parse analyse un corps que vous avez déjà vérifié. Utilisez un switch de type pour accéder aux champs d'un type :
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.
}
Parsecontrôle l'enveloppe (id,type,schema_version,environment_id,occurred_at,recorded_at,seq,data,context) et les membres déclarés par le schéma du type. Un membre manquant ou de type incorrect renvoie un*events.ParseErroravec son chemin, par exempledata.object.reason.- Un type inconnu, ou un type connu avec un
schema_versionque ce SDK ne modélise pas, est renvoyé sous forme de*events.Unknown. Ce n'est pas une erreur. - Les membres non déclarés par le schéma sont conservés :
Raw()renvoie l'événement complet etObjectJSON()renvoiedata.object(en mode de charge utile complète, la représentation de la ressource par l'API de gestion). - Les valeurs des champs énumérés ne sont pas vérifiées par rapport aux valeurs connues, donc une nouvelle valeur envoyée par le serveur ne casse pas l'analyse.
Le package github.com/keenauth/keenauth-go/claims type les revendications d'un jeton vérifié : claims.ParseIDToken pour un jeton d'identité (OpenID Connect Core) et claims.ParseAccessToken pour un jeton d'accès (RFC 9068). Ils contrôlent les types des revendications mais ne vérifient rien ; appelez-les donc sur la map Claims renvoyée par le package verify :
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 est toujours une tranche (claims.Audience). Les revendications qui ne font pas partie du contrat sont vos propres revendications et sont conservées dans Custom.
Erreurs
Une erreur d'API est un document de problème RFC 9457. keenauth.AsProblem(err) le renvoie avec son Code lisible par machine. Un code inconnu est renvoyé tel quel, de sorte que de nouveaux codes serveur ne cassent pas votre client :
if _, _, err := c.UsersAPI.GetUser(ctx, id).Execute(); err != nil {
if pe, ok := keenauth.AsProblem(err); ok && pe.Code == "not_found" {
// …
}
}
Avertissements de dépréciation
Lorsqu'une réponse comporte un en-tête Deprecation (RFC 9745) ou Sunset (RFC 8594), le SDK journalise un avertissement par opération et par valeur d'en-tête pour tout le processus, au niveau WARN du logger défini avec WithLogger. L'avertissement indique la méthode et le chemin, les identifiants étant remplacés par {id}, les valeurs des en-têtes et tout Link avec rel="deprecation" ou rel="sunset". Il ne contient jamais d'identifiants, de jetons ni de clés, et l'appel lui-même n'est pas affecté.
level=WARN msg="keenauth: the API operation is deprecated" operation="GET /users/{id}" deprecation=@1767225600 sunset="Wed, 01 Jul 2026 00:00:00 GMT"