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 :

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.

  1. 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).
  2. 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 :

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 :

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.
}

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"