SDK de servidor para Go
El módulo github.com/keenauth/keenauth-go es el cliente de Go de la API de gestión de KeenAuth. Los métodos tipados para cada recurso de /v1 se generan a partir del documento OpenAPI (paquete management, incluido en keenauth.Client). El núcleo escrito a mano añade autenticación, reintentos, claves de idempotencia, errores tipados y advertencias de obsolescencia. El módulo solo usa la biblioteca estándar. Todavía no está publicado.
Para el inicio de sesión en una aplicación net/http, consulte Sesiones de Go net/http. Para proteger su propia API, consulte Protección de su API.
Crear un cliente con una clave de 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())
La URL base es la dirección de la API de su región seguida de /v1. La clave (idp_sk_… o idp_rk_…) se envía solo en la cabecera Authorization y solo a esa dirección; una redirección a otro host o a http sin cifrar nunca la transmite. El http sin cifrar solo se acepta para localhost.
Opciones:
keenauth.WithHTTPClient(h): su propiohttp.Client(su transporte se envuelve, no se cambia).keenauth.WithTimeout(d): límite de tiempo total de una llamada, incluidos los reintentos (30 segundos por defecto).keenauth.WithClientName("my-tool", "1.4.0"): añade su herramienta aUser-AgenteIdp-Client.keenauth.WithLogger(l): el*slog.Loggerpara las advertencias del SDK (por defecto,slog.Default()).
Autenticarse con private_key_jwt
En lugar de una clave estática, una aplicación de gestión puede iniciar sesión con su propio par de claves. El SDK obtiene un token de gestión del endpoint de tokens de su entorno, lo mantiene en memoria y lo sustituye 60 segundos antes de que caduque. Las llamadas simultáneas comparten una única solicitud de token.
- En el panel, cree una aplicación de gestión y registre su clave pública (como URL de JWKS o directamente; hasta 3 claves activas).
- Guarde la clave privada en su servidor, por ejemplo en un archivo de secretos o en 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", // el emisor de su entorno, exactamente
ClientID: "app_01j9zzzzzzzzzzzzzzzzzzzz",
Key: key,
KeyID: "mgmt-key-2026-11", // el kid que registró; opcional con una sola clave
Scope: "users:read users:write", // opcional; por defecto: los ámbitos predeterminados de la concesión
})
Lo que envía el SDK:
POST <Issuer>/oauth2/tokencongrant_type=client_credentials,client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer, un nuevoclient_assertionyresourcecon la dirección de la API (https://api.eu.keenauth.com; cámbielo conAudience).- La aserción es un JWT con
issysubcon su ID de cliente,audcon el emisor, unjtialeatorio y una duración de 2 minutos. Se firma con ES256 (clave P-256), EdDSA (Ed25519), RS256 o PS256 (RSA, 2048 bits o más; useAlgorithm: "PS256"para PSS).
Key es cualquier crypto.Signer, por lo que una clave almacenada en un KMS o HSM funciona sin exportarla. El token de gestión tiene una validez de 10 minutos. Si la API responde 401, el SDK descarta el token en caché y la siguiente llamada obtiene uno nuevo.
Una solicitud de token rechazada devuelve un *keenauth.TokenError con el estado HTTP y el código error de OAuth, por ejemplo invalid_client para una clave desconocida o un KeyID incorrecto. Nunca contiene la aserción ni un token:
var te *keenauth.TokenError
if errors.As(err, &te) {
log.Printf("token request refused: %d %s", te.Status, te.Code)
}
Reintentos y claves de idempotencia
El SDK reintenta una llamada como máximo 3 veces en total, y solo cuando se cumplen ambas condiciones:
- la respuesta es
429,502,503o504, o la conexión falló (un error de red, no de certificado); - el método es
GET,PUToDELETE, o unPOSTconIdempotency-Key.
Cada operación POST que acepta Idempotency-Key (todas excepto las llamadas sin efectos secundarios users:search y …:preview, y users/{id}:unlock) recibe una nueva clave UUIDv4, y cada reintento de esa llamada envía la misma clave y el mismo cuerpo, de modo que la operación se ejecuta como máximo una vez. Para que una llamada sea idempotente a través de sus propios reinicios, pase su propia clave (de 16 a 64 caracteres de A-Z a-z 0-9 _ -), por ejemplo .IdempotencyKey(orderKey); el SDK nunca la sustituye. Una PATCH que incluye If-Match (toda actualización con versión) solo se reintenta ante 429: el limitador de frecuencia rechaza la solicitud antes de ejecutarla, y If-Match evita que un cambio se aplique dos veces. Ninguna otra PATCH se reintenta nunca.
Entre intentos, el SDK espera lo que indique Retry-After (segundos o una fecha HTTP). Si no existe, espera 0,5 segundos, después 1 segundo, cada uno con una variación de ±25 %. Si Retry-After pide más de 60 segundos, o la espera superaría la fecha límite de su contexto, el SDK devuelve la respuesta en lugar de esperar. Cancelar el contexto detiene la espera.
Recorrer una lista
Los métodos de lista devuelven una página: data y list_metadata con los cursores before y after. keenauth.All sigue after y produce todos los elementos. Solo solicita la página siguiente cuando llega a ella, por lo que salir del bucle detiene las solicitudes posteriores. Configure el tamaño de página y los filtros en la solicitud como de costumbre:
for u, err := range keenauth.All(c.UsersAPI.ListUsers(ctx).Limit(100).Status("active")) {
if err != nil {
return err // la iteración termina después de un error
}
fmt.Println(u.GetId(), u.GetEmail())
}
keenauth.AllBefore sigue before en su lugar, por ejemplo hacia las entradas más recientes de ListLogs. Todos los métodos de lista generados funcionan con ambos, excepto ListEvents de la API de eventos, que pagina con next_cursor y has_more. Un bucle de función por rango necesita Go 1.23 o posterior.
Verificar webhooks
El paquete github.com/keenauth/keenauth-go/webhook comprueba la firma de una entrega de webhook (Standard Webhooks). Proporcione el secreto de firma del endpoint (idp_whsec_…) y pase el cuerpo sin procesar exactamente como se recibió:
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: el secreto no tiene un formato correcto
}
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
}
// Procese el evento. Guarde webhook-id y omita una entrega que ya haya procesado.
w.WriteHeader(http.StatusNoContent)
})
Verify acepta una entrega cuando webhook-timestamp está a menos de 5 minutos de la hora actual (en ambas direcciones) y una de las firmas v1 de webhook-signature coincide. Las firmas se comparan en tiempo constante, y las demás versiones de firma se ignoran. El error indica por qué se rechazó una entrega; cada uno corresponde a uno de webhook.ErrHeaders (falta una cabecera), webhook.ErrTimestamp, webhook.ErrSignature o webhook.ErrSecret, que puede comprobar con errors.Is.
Al rotar el secreto, pase ambos secretos, el nuevo y el antiguo, hasta que el antiguo deje de usarse: webhook.NewVerifier([]string{newSecret, oldSecret}). Durante la rotación, KeenAuth también puede enviar dos firmas en una misma cabecera; basta con que una de ellas sea válida.
Eventos tipados y claims
El paquete github.com/keenauth/keenauth-go/events tiene una estructura por cada tipo de evento de cliente, generada a partir de los esquemas públicos de eventos. webhook.Verifier.VerifyEvent verifica una entrega y la analiza; events.Parse analiza un cuerpo que ya haya verificado. Use un switch por tipo para obtener los campos de un tipo:
ev, err := verifier.VerifyEvent(r.Header, body)
if err != nil {
http.Error(w, "invalid webhook", http.StatusBadRequest) // webhook.Err… o 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:
// Un tipo que esta versión del SDK aún no conoce. e.Type y e.Raw() siguen disponibles.
}
Parsecomprueba el sobre (id,type,schema_version,environment_id,occurred_at,recorded_at,seq,data,context) y los miembros que declara el esquema del tipo. Un miembro ausente o de tipo incorrecto devuelve un*events.ParseErrorcon su ruta, comodata.object.reason.- Un tipo desconocido, o un tipo conocido con un
schema_versionque este SDK no modela, se devuelve como*events.Unknown. No es un error. - Se conservan los miembros que el esquema no declara:
Raw()devuelve el evento completo yObjectJSON()devuelvedata.object(en el modo de carga completa, la representación del recurso de la API de gestión). - Los valores de los campos enumerados no se comprueban contra los valores conocidos, así que un valor nuevo del servidor no interrumpe el análisis.
El paquete github.com/keenauth/keenauth-go/claims tipifica los claims de un token verificado: claims.ParseIDToken para un ID token (OpenID Connect Core) y claims.ParseAccessToken para un access token (RFC 9068). Comprueban los tipos de los claims, pero no verifican nada, así que llámelos sobre el mapa Claims que devuelve el paquete 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"] // sus propios claims
aud es siempre un slice (claims.Audience). Los claims que no forman parte del contrato son sus propios claims y se conservan en Custom.
Errores
Un error de la API es un documento de problema RFC 9457. keenauth.AsProblem(err) lo devuelve con el Code legible por máquina. Un código desconocido se devuelve tal cual, así que los nuevos códigos del servidor no interrumpen su cliente:
if _, _, err := c.UsersAPI.GetUser(ctx, id).Execute(); err != nil {
if pe, ok := keenauth.AsProblem(err); ok && pe.Code == "not_found" {
// …
}
}
Advertencias de obsolescencia
Cuando una respuesta incluye una cabecera Deprecation (RFC 9745) o Sunset (RFC 8594), el SDK registra una advertencia por operación y valor de cabecera para todo el proceso, con nivel WARN en el logger que configuró con WithLogger. La advertencia indica el método y la ruta con los identificadores sustituidos por {id}, los valores de las cabeceras y cualquier Link con rel="deprecation" o rel="sunset". Nunca contiene IDs, tokens ni claves, y la llamada en sí no se ve afectada.
level=WARN msg="keenauth: the API operation is deprecated" operation="GET /users/{id}" deprecation=@1767225600 sunset="Wed, 01 Jul 2026 00:00:00 GMT"