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

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.

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.

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