
# 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](/docs/sdk/go-nethttp). Pour protéger votre propre API, voir [Protéger votre API](/docs/sdk/protect-api).

## Créer un client avec une clé d'API

```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())
```

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 propre `http.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-Agent` et `Idp-Client`.
- `keenauth.WithLogger(l)` : le `*slog.Logger` pour 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.

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.

```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
})
```

Ce que le SDK envoie :

- `POST <Issuer>/oauth2/token` avec `grant_type=client_credentials`, `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`, une nouvelle `client_assertion` et `resource` défini sur l'adresse de l'API (`https://api.eu.keenauth.com` ; modifiable avec `Audience`).
- L'assertion est un JWT dont `iss` et `sub` valent votre identifiant client, `aud` vaut l'émetteur, `jti` est 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éfinissez `Algorithm: "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 :

```go
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`, `503` ou `504`, ou la connexion a échoué (erreur réseau, pas erreur de certificat) ;
- la méthode est `GET`, `PUT` ou `DELETE`, ou un `POST` avec un `Idempotency-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 :

```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` 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 :

```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` 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 :

```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` contrô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.ParseError` avec son chemin, par exemple `data.object.reason`.
- Un type inconnu, ou un type connu avec un `schema_version` que 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 et `ObjectJSON()` renvoie `data.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` :

```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` 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 :

```go
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é.

```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"
```
