SDK server Go

Il modulo github.com/keenauth/keenauth-go è il client Go dell'API di gestione di KeenAuth. I metodi tipizzati per ogni risorsa /v1 sono generati dal documento OpenAPI (pacchetto management, incluso in keenauth.Client). Il core scritto a mano aggiunge autenticazione, retry, chiavi di idempotenza, errori tipizzati e avvisi di deprecazione. Il modulo usa solo la libreria standard. Non è ancora pubblicato.

Per accedere in un'app net/http, vedi Sessioni Go net/http. Per proteggere la tua API, vedi Proteggere la tua API.

Crea un client con una API key

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 di base è l'indirizzo API della tua regione seguito da /v1. La chiave (idp_sk_… o idp_rk_…) viene inviata solo nell'header Authorization e solo a quell'indirizzo; un redirect verso un altro host o verso http semplice non la trasporta mai. Il protocollo http semplice è accettato solo per localhost.

Opzioni:

Autentica con private_key_jwt

Invece di una chiave statica, un'applicazione di gestione può accedere con una propria coppia di chiavi. L'SDK ottiene un token di gestione dall'endpoint token del tuo ambiente, lo tiene in memoria e lo sostituisce 60 secondi prima della scadenza. Le chiamate concorrenti condividono una sola richiesta di token.

  1. Nella dashboard, crea un'applicazione di gestione e registra la sua chiave pubblica (come URL JWKS o direttamente; fino a 3 chiavi attive).
  2. Conserva la chiave privata sul tuo server, per esempio in un file segreto o in 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
})

Cosa invia l'SDK:

Key accetta qualsiasi crypto.Signer, quindi una chiave conservata in un KMS o HSM funziona senza essere esportata. Il token di gestione dura 10 minuti. Se l'API risponde 401, l'SDK scarta il token in cache e la chiamata successiva ne ottiene uno nuovo.

Una richiesta di token rifiutata restituisce un *keenauth.TokenError con lo stato HTTP e il codice error OAuth, per esempio invalid_client per una chiave sconosciuta o un KeyID errato. Non contiene mai l'assertion né un token:

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

Retry e chiavi di idempotenza

L'SDK ritenta una chiamata al massimo 3 volte in totale, e solo quando sono vere entrambe le condizioni:

Ogni operazione POST che accetta un Idempotency-Key (tutte tranne le chiamate users:search e …:preview, prive di effetti collaterali, e users/{id}:unlock) riceve una nuova chiave UUIDv4, e ogni retry di quella chiamata invia la stessa chiave e lo stesso corpo, quindi l'operazione viene eseguita al massimo una volta. Per rendere una chiamata idempotente anche attraverso i riavvii del tuo sistema, passa una chiave tua (da 16 a 64 caratteri di A-Z a-z 0-9 _ -), per esempio .IdempotencyKey(orderKey); l'SDK non la sostituisce mai. Un PATCH che include If-Match (ogni aggiornamento con versione) viene ritentato solo su 429: il rate limiter rifiuta la richiesta prima che venga eseguita, e If-Match evita che una modifica venga applicata due volte. Qualsiasi altro PATCH non viene mai ritentato.

Tra un tentativo e l'altro l'SDK attende quanto indicato da Retry-After (secondi o data HTTP). Senza, attende 0,5 secondi, poi 1 secondo, ciascuno con un jitter di ±25 %. Se Retry-After chiede più di 60 secondi, o l'attesa supererebbe la scadenza del tuo context, l'SDK restituisce la risposta invece di attendere. Annullare il context interrompe l'attesa.

Scorri una lista

I metodi di lista restituiscono una sola pagina: data e list_metadata con i cursori before e after. keenauth.All segue after e restituisce ogni elemento. Recupera la pagina successiva solo quando la raggiungi, quindi uscire dal ciclo interrompe le richieste successive. Imposta dimensione della pagina e filtri sulla richiesta come al solito:

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 segue invece before, per esempio verso le voci più recenti di ListLogs. Ogni metodo di lista generato funziona con entrambi, eccetto ListEvents dell'API Events, che pagina con next_cursor e has_more. Un ciclo range-over-func richiede Go 1.23 o successivo.

Verifica i webhook

Il pacchetto github.com/keenauth/keenauth-go/webhook verifica la firma di una consegna webhook (Standard Webhooks). Passagli il segreto di firma dell'endpoint (idp_whsec_…) e passa il corpo grezzo esattamente come ricevuto:

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 accetta una consegna quando webhook-timestamp è entro 5 minuti da adesso (in entrambe le direzioni) e una delle firme v1 in webhook-signature corrisponde. Le firme vengono confrontate in tempo costante, e le altre versioni di firma vengono ignorate. L'errore indica perché una consegna è stata rifiutata; ciascuno corrisponde a uno di webhook.ErrHeaders (manca un header), webhook.ErrTimestamp, webhook.ErrSignature o webhook.ErrSecret, verificabili con errors.Is.

Quando ruoti il segreto, passa entrambi, nuovo e vecchio, finché quello vecchio non viene più usato: webhook.NewVerifier([]string{newSecret, oldSecret}). Durante la rotazione KeenAuth può inviare anche due firme in un header; basta che una sia valida.

Eventi tipizzati e claim

Il pacchetto github.com/keenauth/keenauth-go/events contiene una struct per ogni tipo di evento cliente, generata dagli schemi pubblici degli eventi. webhook.Verifier.VerifyEvent verifica una consegna e la analizza; events.Parse analizza un corpo che hai già verificato. Usa uno switch sui tipi per ottenere i campi di un tipo:

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

Il pacchetto github.com/keenauth/keenauth-go/claims tipizza i claim di un token verificato: claims.ParseIDToken per un ID token (OpenID Connect Core) e claims.ParseAccessToken per un access token (RFC 9068). Controllano i tipi dei claim ma non verificano nulla, quindi chiamali sulla mappa Claims restituita dal pacchetto 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 è sempre una slice (claims.Audience). I claim che non fanno parte del contratto sono tuoi claim personalizzati e vengono conservati in Custom.

Errori

Un errore dell'API è un documento problem RFC 9457. keenauth.AsProblem(err) lo restituisce con il Code leggibile dalla macchina. Un codice sconosciuto viene restituito così com'è, quindi nuovi codici del server non interrompono il tuo client:

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

Avvisi di deprecazione

Quando una risposta contiene un header Deprecation (RFC 9745) o Sunset (RFC 8594), l'SDK registra un avviso per operazione e valore dell'header per l'intero processo, a livello WARN sul logger impostato con WithLogger. L'avviso indica il metodo e il percorso con gli identificativi sostituiti da {id}, i valori degli header e ogni Link con rel="deprecation" o rel="sunset". Non contiene mai ID, token o chiavi, e la chiamata stessa non viene modificata.

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