Offline testen mit dem Test-Kit
Jedes Server-SDK (Go, TypeScript und Python) enthält ein testing-Modul für Unit-Tests und CI. Es erzeugt Zugriffstoken und ID-Token, die mit einem festen Testschlüssel signiert sind, baut einen Token-Verifier, der genau diese Token ohne jeglichen Netzwerkzugriff akzeptiert, stellt Beispiel-Nutzdaten für Ereignisse bereit und signiert Webhook-Zustellungen mit einem öffentlichen Test-Secret. Ihre Tests laufen ohne KeenAuth-Umgebung, ohne Zugangsdaten und ohne Internetverbindung.
So funktioniert es
- Ein fester, öffentlicher Testschlüssel. Test-Token werden mit ES256 und der Schlüssel-ID
keenauth-test-1signiert. Der Schlüssel ist bewusst öffentlich: Sein privater Teil wird aus einer veröffentlichten Bezeichnung abgeleitet, und der öffentliche Schlüsselsatz ist in jedem SDK enthalten. Vertrauen Sie diesem Schlüssel an keiner anderen Stelle. - Ein Aussteller, der nicht existieren kann. Test-Token tragen den Aussteller
https://test.keenauth.invalid. Die Top-Level-Domain.invalidist reserviert (RFC 2606) und wird nie aufgelöst, daher kann keine echte Umgebung diesen Aussteller haben. - Expliziter Testmodus. Der SDK-Verifier akzeptiert diesen Aussteller nur, wenn der Testmodus im Code aktiviert ist. Ein Verifier, der auf die normale Weise konfiguriert ist, lehnt jeden Aussteller unter
.invalidals Konfigurationsfehler ab. So kann ein Produktions-Verifier niemals ein Test-Token akzeptieren, selbst wenn sein Aussteller versehentlich auf den Test-Aussteller gesetzt wäre. Ein mit dem Testschlüssel signiertes Token, das jedoch Ihren echten Aussteller trägt, wird ebenfalls abgelehnt, denn Ihre Umgebung veröffentlicht andere Schlüssel. - Kein Netzwerkzugriff. Im Testmodus entnimmt der Verifier den Testschlüssel dem Speicher und ruft nichts ab. Er akzeptiert nur den Test-Aussteller.
Der Testmodus schwächt die übrigen Prüfungen nicht ab: Abgelaufene Token, eine falsche Audience, eine veränderte Nutzlast, ein fehlendes jti und ein DPoP-gebundenes Token ohne Nachweis werden genauso abgelehnt wie in der Produktion. Die vollständige Liste finden Sie unter Token in Ihrer API verifizieren.
Verwenden Sie den Testmodus nur im Testcode. Jeder kann Test-Token signieren, daher würde ein Dienst, der ihn aktiviert, gefälschte Token akzeptieren.
Go
Das Paket heißt keenauthtest und folgt dem Stil von net/http/httptest.
import (
"github.com/keenauth/keenauth-go/keenauthtest"
"github.com/keenauth/keenauth-go/verify"
)
func TestOrdersAPI(t *testing.T) {
v, err := keenauthtest.NewVerifier(verify.Config{Audience: "https://api.acme.example"})
if err != nil {
t.Fatal(err)
}
token, err := keenauthtest.AccessToken("https://api.acme.example",
keenauthtest.WithClaims(map[string]any{"scope": "orders:read", "org_id": "org_01m3vnbwg0fvav6zw6w7v3st0r"}))
if err != nil {
t.Fatal(err)
}
// Pass v to the handler under test and send "Authorization: Bearer " + token.
claims, err := v.VerifyAccessToken(context.Background(), token, nil)
if err != nil || claims.Scope != "orders:read" {
t.Fatal(err)
}
}
keenauthtest.IDToken(clientID, ...) erzeugt ein ID-Token. WithLifetime ändert die Gültigkeitsdauer (ein negativer Wert ergibt ein abgelaufenes Token), und WithIssuedAt legt den Ausstellungszeitpunkt fest. In WithClaims entfernt ein nil-Wert einen Claim. Ohne das Paket können Sie verify.Config{TestingMode: true} selbst setzen.
TypeScript
Das Modul ist der Unterpfad @keenauth/node/testing.
import { createTestVerifier, mintAccessToken } from "@keenauth/node/testing";
const verifier = createTestVerifier({ audience: "https://api.acme.example" });
const token = mintAccessToken("https://api.acme.example", {
claims: { scope: "orders:read", org_id: "org_01m3vnbwg0fvav6zw6w7v3st0r" },
});
const claims = await verifier.verifyAccessToken(token);
// claims.subject, claims.scope, claims.claims.org_id
mintIdToken(clientId, ...) erzeugt ein ID-Token. Die Optionen sind claims (ein null-Wert entfernt einen Claim), lifetimeSeconds und issuedAt. Ohne das Modul können Sie testing: true an createVerifier übergeben.
Python
Das Modul heißt keenauth.testing. Wie der Verifier benötigt es das Extra verify.
from keenauth import testing
verifier = testing.verifier(audience="https://api.acme.example")
token = testing.mint_access_token(
"https://api.acme.example",
claims={"scope": "orders:read", "org_id": "org_01m3vnbwg0fvav6zw6w7v3st0r"},
)
claims = verifier.verify_access_token(token)
# claims.subject, claims.scope, claims.claims["org_id"]
testing.mint_id_token(client_id, ...) erzeugt ein ID-Token. Die Schlüsselwortargumente sind claims (ein None-Wert entfernt einen Claim), lifetime_seconds und issued_at. Ohne das Modul können Sie testing=True an VerifierConfig übergeben.
Standard-Claims
| Claim | Zugriffstoken | ID-Token |
|---|---|---|
iss |
https://test.keenauth.invalid |
https://test.keenauth.invalid |
sub |
usr_01m3vnbwg0e8mbe7h7xxp6rmkc |
usr_01m3vnbwg0e8mbe7h7xxp6rmkc |
aud |
die von Ihnen übergebene Audience | die von Ihnen übergebene Client-ID |
client_id |
app_01m3vnbwg0fqba5ntanfd63s8n |
— |
iat, exp |
jetzt, jetzt + 3600 Sekunden | jetzt, jetzt + 3600 Sekunden |
jti |
zufällig | — |
auth_time |
— | wie iat |
Der Header des Zugriffstokens enthält typ: at+jwt (RFC 9068). Fügen Sie die Claims hinzu, die Ihre API liest, etwa scope, org_id, permissions, acr, amr oder auth_time. Bei ID-Token fügen Sie nonce hinzu, wenn Ihr Code es prüft.
Beispielereignisse
Das Kit enthält Beispiel-Nutzdaten für gängige Ereignisse: user.created, user.updated, user.deleted, session.created, session.revoked, organization.created und organization_membership.created. Sie verwenden denselben Envelope und dieselben Schemas wie die Ereignisse, die KeenAuth sendet, im Standardmodus „thin“: Kennungen, Zustand und Namen geänderter Felder, keine personenbezogenen Daten. Verwenden Sie sie, um Ihre Ereignis-Handler zu testen.
- Go:
keenauthtest.Event("user.created")gibt das JSON zurück;keenauthtest.EventTypes()listet die Typen auf. - TypeScript:
sampleEvent("user.created")undsampleEventTypes(). - Python:
testing.sample_event("user.created")undtesting.event_types().
Webhooks testen
Das Kit signiert die Nutzdaten genau wie eine KeenAuth-Webhook-Zustellung (Standard Webhooks v1). Es gibt den rohen Body sowie die Header webhook-id, webhook-timestamp und webhook-signature zurück. Ihr Handler verifiziert sie mit dem Webhook-Verifier des SDK, demselben Code, der in der Produktion läuft. Nur das Secret unterscheidet sich.
- Ein öffentliches Test-Secret. Standardmäßig signiert das Kit mit einem festen
idp_whsec_-Test-Secret, dessen Schlüssel aus der veröffentlichten BezeichnungKeenAuth offline test kit webhook secret (public, TEST ONLY)abgeleitet wird. Jeder kann es berechnen, konfigurieren Sie es daher niemals an einem echten Endpunkt. Konfigurieren Sie Ihren Verifier in Tests damit oder übergeben Sie dem Helfer Ihr eigenes Secret. - Wie bei einer echten Zustellung.
webhook-idist dieiddes Ereignisses (oder eine zufälligemsg_-ID, wenn die Nutzdaten keine haben), undwebhook-timestampist der aktuelle Zeitpunkt, sofern Sie keine Werte setzen. Der Body wird genau so gesendet, wie Sie ihn übergeben. - Wiederholung und Manipulation. Signieren Sie mit einem alten Zeitstempel, um zu testen, ob Ihr Handler Zustellungen außerhalb des 5-Minuten-Fensters ablehnt, und verändern Sie den Body nach dem Signieren, um zu testen, ob er eine ungültige Signatur ablehnt.
Node
import { Webhook, WebhookVerificationError } from "@keenauth/node/webhooks";
import { sampleEvent, signWebhook, testWebhookSecret } from "@keenauth/node/testing";
const webhook = new Webhook(testWebhookSecret()); // in production: process.env.KEENAUTH_WEBHOOK_SECRET
const delivery = signWebhook(sampleEvent("user.created"));
const event = webhook.verify(delivery.body, delivery.headers); // the parsed event
// A delivery from an hour ago is refused.
const old = signWebhook(sampleEvent("user.created"), { timestamp: Math.floor(Date.now() / 1000) - 3600 });
try {
webhook.verify(old.body, old.headers);
} catch (e) {
// e instanceof WebhookVerificationError, e.kind === "timestamp"
}
signWebhook(payload, options) nimmt einen String, ein Uint8Array oder ein Objekt entgegen (serialisiert mit JSON.stringify). Die Optionen sind secret, id und timestamp (Unix-Sekunden oder ein Date). Um einen HTTP-Handler zu testen, senden Sie delivery.body als rohen Request-Body mit den Headern delivery.headers und Content-Type: application/json.
Python
import time
from keenauth import testing
from keenauth.webhooks import WebhookTimestampError, WebhookVerifier
verifier = WebhookVerifier(testing.webhook_secret()) # in production: os.environ["KEENAUTH_WEBHOOK_SECRET"]
delivery = testing.sign_webhook(testing.sample_event("user.created"))
verifier.verify(delivery.body, delivery.headers)
# A delivery from an hour ago is refused.
old = testing.sign_webhook(testing.sample_event("user.created"), timestamp=int(time.time()) - 3600)
try:
verifier.verify(old.body, old.headers)
except WebhookTimestampError:
pass
testing.sign_webhook(payload, secret=None, msg_id=None, timestamp=None) nimmt Bytes, einen String oder ein Dictionary entgegen (serialisiert als kompaktes JSON) und gibt delivery.body (Bytes) und delivery.headers (ein Dictionary) zurück. Mit einem Test-Client wie dem von Flask oder FastAPI senden Sie delivery.body als rohen Body mit delivery.headers.
Go
import (
"net/http"
"net/http/httptest"
"github.com/keenauth/keenauth-go/keenauthtest"
"github.com/keenauth/keenauth-go/webhook"
)
func TestWebhookHandler(t *testing.T) {
v, err := webhook.NewVerifier([]string{keenauthtest.WebhookSecret()}) // in production: os.Getenv("KEENAUTH_WEBHOOK_SECRET")
if err != nil {
t.Fatal(err)
}
body, err := keenauthtest.Event("user.created")
if err != nil {
t.Fatal(err)
}
delivery, err := keenauthtest.SignWebhook(body)
if err != nil {
t.Fatal(err)
}
req, err := delivery.Request("http://localhost/webhooks/keenauth")
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
newWebhookHandler(v).ServeHTTP(rec, req) // your handler, which calls v.Verify(r.Header, body)
if rec.Code != http.StatusOK {
t.Fatalf("status %d", rec.Code)
}
}
keenauthtest.SignWebhook(payload, ...) gibt delivery.Body und delivery.Header zurück; delivery.Request(url) erstellt die POST-Anfrage mit Content-Type: application/json. Die Optionen sind WithWebhookSecret, WithWebhookID und WithWebhookTimestamp.
Was das Kit nicht leistet
- Es emuliert KeenAuth nicht. Anmeldeabläufe, Regeln für die Token-Ausstellung, Ratenbegrenzungen und jedes andere serverseitige Verhalten sind nur in Ihrer gehosteten Umgebung
developmentecht. Führen Sie Ihre Integrationstests dort aus, bevor Sie ausliefern. - Es gibt keine Test-Uhr. Um die Zeit zu steuern, übergeben Sie dem Verifier Ihre eigene Uhr (
Now,now) und legen den Ausstellungszeitpunkt des Tokens fest. - Es erstellt keine DPoP-Nachweise. Sie können einen Claim
cnf.jkthinzufügen, müssen den Nachweis dann aber selbst signieren. - Es liefert keine Webhooks aus. Echte Zustellungen, Wiederholungen und die Auswahl der Ereignisse sind nur in Ihrer gehosteten Umgebung
developmentecht.