SDK de gestion pour Node.js
@keenauth/node est le SDK TypeScript de l'API de gestion KeenAuth. Il nécessite Node.js 22 ou une version ultérieure et n'a aucune dépendance d'exécution. Il n'est pas encore publié sur npm.
Le package dispose des points d'entrée suivants :
| Import | Utilisation |
|---|---|
@keenauth/node |
Le client de l'API de gestion, les jetons de gestion, la pagination et les erreurs |
@keenauth/node/webhooks |
La vérification des signatures des webhooks, sans charger le client de l'API |
@keenauth/node/bff |
La connexion pour les applications serveur : Express et Fastify |
@keenauth/node/middleware |
La protection de votre API avec des jetons porteurs |
@keenauth/node/testing |
Le kit de test hors ligne |
Créer un client
Avec une clé d'API secrète ou restreinte :
import { KeenAuth } from "@keenauth/node";
const keenauth = new KeenAuth({
baseUrl: "https://api.eu.keenauth.com/v1",
apiKey: process.env.KEENAUTH_API_KEY!,
});
const user = await keenauth.users.getUser({ userId: "usr_01j9zzzzzzzzzzzzzzzzzzzz" });
baseUrl est l'adresse de l'API de votre région suivie de /v1. Elle doit utiliser https ; http n'est accepté que pour localhost. La clé n'est envoyée que dans l'en-tête Authorization et uniquement à cette adresse. Les redirections ne sont pas suivies.
S'authentifier sans clé d'API
Une application management peut appeler l'API avec un jeton de courte durée au lieu d'une clé statique. L'application signe une assertion client avec sa clé privée (private_key_jwt, RFC 7523), et le SDK l'échange auprès du point de terminaison de jeton de votre environnement.
- Dans le tableau de bord, créez une application de type
managementainsi qu'un enregistrement de client de gestion avec les autorisations dont elle a besoin (voir Créer un client de gestion). - Générez une paire de clés et enregistrez la clé publique comme identifiant de type
public_keyde l'application. Par exemple, pour ES256 :openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out management-key.pem. - Conservez la clé privée dans votre coffre de secrets. Transmettez-la au SDK :
import { readFileSync } from "node:fs";
import { KeenAuth } from "@keenauth/node";
const keenauth = new KeenAuth({
baseUrl: "https://api.eu.keenauth.com/v1",
credentials: {
issuer: "https://acme.eu.keenauth.app", // l'émetteur de votre environnement, à l'identique
clientId: "app_01j9zzzzzzzzzzzzzzzzzzzz",
privateKey: readFileSync("management-key.pem", "utf8"), // PEM, un JWK privé ou un KeyObject
scope: ["users:read", "users:write"], // facultatif ; par défaut : les scopes par défaut de l'application
},
});
Ce que fait le SDK :
- Il envoie
grant_type=client_credentialsà<issuer>/oauth2/tokenavecresourcedéfini sur l'identifiant de l'API de gestion. La valeur par défaut est l'origine debaseUrl, par exemplehttps://api.eu.keenauth.com; définissezcredentials.audiencepour la modifier. - Chaque requête reçoit une nouvelle assertion client :
issetsubcorrespondent à l'identifiant client,audest l'émetteur,jtiest aléatoire et l'assertion est valable 120 secondes. L'en-têtekidest l'empreinte RFC 7638 de la clé, ce qui permet à KeenAuth de retrouver la clé lorsque l'application en possède plusieurs (pour la rotation des clés). - L'algorithme dépend de la clé : les clés RSA signent avec
RS256(définissezalg: "PS256"pour RSA-PSS), les clés P-256 avecES256et les clés Ed25519 avecEdDSA. Les clés RSA doivent comporter au moins 2048 bits. - Le jeton d'accès est mis en cache et renouvelé 60 secondes avant son expiration. Les appels simultanés partagent un même renouvellement. Si l'API répond
401, le jeton en cache est supprimé et l'appel suivant en obtient un nouveau.
Une requête de jeton en échec lève une TokenError contenant le code OAuth (par exemple invalid_client ou invalid_scope), la description et le status. Une erreur de configuration, comme une clé publique fournie à la place d'une clé privée, est levée lors de la création du client.
apiKey et credentials ne peuvent pas être utilisés ensemble.
Nouvelles tentatives
Le SDK relance une requête au maximum 3 fois au total lorsque l'API répond 429, 502, 503 ou 504, ou lorsque la connexion échoue. Il ne relance que les requêtes GET, PUT et DELETE, ainsi que les requêtes POST qui portent un Idempotency-Key. Une requête PATCH qui porte If-Match n'est relancée que sur 429 : le limiteur de débit refuse la 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é.
- Si la réponse contient un en-tête
Retry-After(en secondes ou sous forme de date HTTP), le SDK attend ce délai. UnRetry-Aftersupérieur à 60 secondes n'est pas attendu : le SDK vous renvoie cette réponse. - Sinon, il attend 0,5 seconde, puis 1 seconde, chacune avec une gigue aléatoire de ±25 %.
timeoutMs(30 secondes par défaut) limite la durée totale de l'appel, nouvelles tentatives comprises. Si une attente devait dépasser cette limite, le SDK renvoie la dernière réponse à la place.
Le SDK ajoute un Idempotency-Key aléatoire à chaque POST dont l'opération accepte cet en-tête, afin qu'une création relancée ne produise pas un second objet. Pour rendre vos propres nouvelles tentatives sûres également, transmettez vous-même la même clé :
await keenauth.users.createUser({
userCreateRequest: { email: "ada@example.com" },
idempotencyKey: "signup-7d1f0c2a-0d7e-4c55",
});
Pagination
Les points de terminaison de liste renvoient une seule page : data et listMetadata avec les curseurs before et after. paginate parcourt les curseurs à votre place :
import { paginate } from "@keenauth/node";
for await (const user of paginate((p) => keenauth.users.listUsers(p), { limit: 100 })) {
console.log(user.id);
if (user.email === "ada@example.com") break; // s'arrête sans charger la page suivante
}
- Sans
before, il avance avecafterjusqu'à ce que le curseur vaillenull. Avecbefore, il recule. limitcorrespond à la taille de page (1 à 100) et est envoyé à chaque requête ; les autres paramètres sont conservés.paginatePagesrenvoie des pages entières au lieu d'éléments.- Si le serveur renvoie deux fois le même curseur, la boucle s'arrête avec une
PaginationError.
Erreurs
Une réponse d'erreur devient une ProblemError (RFC 9457) avec status, code, title, detail, requestId et les errors par champ. Vérifiez code, et non le message. Les codes que le SDK ne connaît pas restent disponibles sous forme de chaînes.
import { ProblemError } from "@keenauth/node";
try {
await keenauth.users.getUser({ userId });
} catch (e) {
if (e instanceof ProblemError && e.code === "not_found") {
// …
}
}
Une réponse sans document de problème, comme une page HTML provenant d'un proxy, devient la ResponseError générée, avec la response brute.
Vérifier les webhooks
KeenAuth signe les webhooks selon le schéma Standard Webhooks v1. Vérifiez la signature à partir du corps brut de la requête, avant de l'analyser :
import express from "express";
import { Webhook, WebhookVerificationError } from "@keenauth/node/webhooks";
const webhook = new Webhook(process.env.KEENAUTH_WEBHOOK_SECRET!); // idp_whsec_…
const app = express();
app.post("/webhooks/keenauth", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = webhook.verify(req.body, req.headers);
} catch (e) {
if (e instanceof WebhookVerificationError) return res.status(400).send(e.kind);
throw e;
}
// traiter event.type, puis répondre rapidement
res.sendStatus(204);
});
- Le SDK vérifie les en-têtes
webhook-id,webhook-timestampetwebhook-signature. L'horodatage doit se situer à 5 minutes près de l'horloge de votre serveur, dans un sens comme dans l'autre. - Pendant une rotation de secret, transmettez les deux secrets :
new Webhook([newSecret, oldSecret]). Une requête est acceptée dès qu'une signaturev1correspond à l'un des secrets. - Les signatures sont comparées en temps constant.
e.kindindique ce qui a échoué :headers(un en-tête est absent),timestamp(non numérique ou hors de la tolérance),signature(aucune signature ne correspond) ousecret(le secret configuré n'est pas une valeuridp_whsec_valide, erreur de configuration levée parnew Webhook). Chaque type possède aussi sa propre classe, commeWebhookSignatureError.verifyrenvoie le corps JSON analysé.verifyRawrenvoie les octets vérifiés.
Les webhooks peuvent arriver plusieurs fois. Utilisez webhook-id pour ignorer les doublons.
Événements typés et revendications
@keenauth/node/events dispose d'un type pour chaque événement client, généré à partir des schémas publics des événements. parseEvent vérifie un corps vérifié et renvoie KeenAuthEvent | UnknownEvent. Après isKnownEvent, switch (event.type) restreint l'événement à un type précis. verifyEvent dans @keenauth/node/webhooks effectue ces deux étapes :
import { verifyEvent } from "@keenauth/node/webhooks";
import { isKnownEvent } from "@keenauth/node/events";
const event = verifyEvent(webhook, req.body, req.headers); // WebhookVerificationError ou EventParseError
if (!isKnownEvent(event)) {
// Un type que cette version du SDK ne connaît pas encore. event.type et event.data.object.id restent typés.
return res.sendStatus(204);
}
switch (event.type) {
case "user.created":
console.log(event.data.object.id);
break;
case "email_sending.suspended":
console.log(event.data.object.reason); // "bounce_rate" | "complaint_rate" | (string & {})
break;
}
parseEventaccepte du texte JSON, des octets ou une valeur déjà décodée. Il vérifie l'enveloppe et les membres que le schéma du type déclare. Un membre absent ou d'un type incorrect lève uneEventParseErroravec son chemin, par exempledata.object.reason.- Un type inconnu, ou un type connu avec un
schema_versionque ce SDK ne modélise pas, donne unUnknownEvent. Ce n'est pas une erreur. - Les membres non déclarés dans le schéma restent sur l'objet.
- Les champs énumérés sont typés avec les valeurs connues plus
string, si bien qu'une nouvelle valeur envoyée par le serveur reste compatible avec le typage et l'analyse.
@keenauth/node/claims type les revendications d'un jeton vérifié : parseIdTokenClaims (OpenID Connect Core) et parseAccessTokenClaims (RFC 9068). Elles contrôlent les types des revendications mais ne vérifient rien ; appelez-les donc sur les claims renvoyés par le vérificateur :
import { parseAccessTokenClaims } from "@keenauth/node/claims";
const at = await verifier.verifyAccessToken(token);
const claims = parseAccessTokenClaims(at.claims);
claims.client_id; // string
claims.org_roles; // string[] | undefined
claims["tenant_tier"]; // unknown : vos propres revendications
Une revendication non conforme lève une ClaimsParseError avec son nom.
Avertissements de dépréciation
Lorsqu'une réponse contient un en-tête Deprecation (RFC 9745) ou Sunset (RFC 8594), le SDK écrit un avertissement par point de terminaison et par valeur d'en-tête pendant toute la durée du processus. L'avertissement indique la méthode, le chemin dont les identifiants sont remplacés par {id}, ainsi que les valeurs des en-têtes ; il ne contient jamais de jetons, de chaînes de requête ni de données utilisateur. Il ne lève jamais d'exception.
Par défaut, l'avertissement est transmis à process.emitWarning avec le type KeenAuthDeprecationWarning. Pour l'envoyer à votre journal à la place :
const keenauth = new KeenAuth({
baseUrl: "https://api.eu.keenauth.com/v1",
apiKey: process.env.KEENAUTH_API_KEY!,
onWarning: (message) => logger.warn(message),
});
En-tête de télémétrie
Chaque requête contient Idp-Client: keenauth-ts/<version> (node/<version>). Cet en-tête ne contient aucune donnée personnelle et ne peut pas être désactivé ; KeenAuth l'utilise pour l'assistance et pour vous informer des versions du SDK devenues obsolètes.