テストキットでオフラインテストする

各サーバー用SDK(Go、TypeScript、Python)には、単体テストとCI用の testing モジュールがあります。このモジュールは、固定のテスト用鍵で署名したアクセストークンとIDトークンを発行し、ネットワークアクセスなしでそれらのトークンだけを受け入れるトークン検証器を構築します。また、イベントのサンプルペイロードを提供し、公開されたテスト用シークレットでWebhook配信に署名します。テストは、KeenAuth環境、認証情報、インターネット接続がなくても実行できます。

仕組み

テストモードでは、ほかの検査が弱まることはありません。有効期限切れのトークン、誤ったオーディエンス、変更されたペイロード、jti の欠落、証明のないDPoPバインドトークンは、本番環境と同様に拒否されます。全一覧はAPI でのトークンの検証を参照してください。

テストモードは、テストコードの中でのみ有効にしてください。テストトークンは誰でも署名できるため、テストモードを有効にしたサービスは偽造されたトークンを受け入れてしまいます。

Go

パッケージは keenauthtest で、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, ...) はIDトークンを発行します。WithLifetime は有効期間を変更し(負の値を指定すると有効期限切れのトークンになります)、WithIssuedAt は発行時刻を設定します。WithClaims では nil の値によってクレームを削除します。パッケージを使わない場合は、verify.Config{TestingMode: true} を自分で設定できます。

TypeScript

モジュールはサブパス @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, ...) はIDトークンを発行します。オプションは claims(null の値はクレームを削除します)、lifetimeSeconds および issuedAt です。モジュールを使わない場合は、testing: true を createVerifier に渡せます。

Python

モジュールは keenauth.testing です。検証器と同様に 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, ...) はIDトークンを発行します。キーワード引数は claims(None の値はクレームを削除します)、lifetime_seconds および issued_at です。モジュールを使わない場合は、testing=True を VerifierConfig に渡せます。

既定のクレーム

クレーム アクセストークン IDトークン
iss https://test.keenauth.invalid https://test.keenauth.invalid
sub usr_01m3vnbwg0e8mbe7h7xxp6rmkc usr_01m3vnbwg0e8mbe7h7xxp6rmkc
aud 渡したオーディエンス 渡したクライアントID
client_id app_01m3vnbwg0fqba5ntanfd63s8n —
iat, exp 現在時刻、現在時刻 + 3600秒 現在時刻、現在時刻 + 3600秒
jti ランダム —
auth_time — iat と同じ

アクセストークンのヘッダーには typ: at+jwt があります(RFC 9068)。API が読み取るクレーム(scope、org_id、permissions、acr、amr、auth_time など)を追加してください。IDトークンについては、コードがチェックする場合は nonce を追加してください。

サンプルイベント

キットには、よく使われるイベントのサンプルペイロードが含まれています:user.created、user.updated、user.deleted、session.created、session.revoked、organization.created および organization_membership.created。これらは、KeenAuth が送信するイベントと同じエンベロープとスキーマを、既定の thin モードで使用します。識別子、状態、変更されたフィールド名のみを含み、個人データは含みません。イベントハンドラーのテストに使用してください。

Webhook のテスト

キットは、KeenAuth の Webhook 配信(Standard Webhooks v1)とまったく同じ方法でペイロードに署名します。元の本文と webhook-id、webhook-timestamp および webhook-signature ヘッダーを返します。ハンドラーは、本番環境で実行されるものと同じコードである SDK の Webhook 検証器で検証します。異なるのはシークレットだけです。

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) は文字列、Uint8Array、またはオブジェクト(JSON.stringify でシリアライズ)を受け取ります。オプションは secret、id および timestamp(Unix秒または Date)です。HTTP ハンドラーをテストするには、delivery.body を生のリクエスト本文として、delivery.headers と 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) は bytes、文字列、または辞書(コンパクトな JSON としてシリアライズ)を受け取り、delivery.body(bytes)と delivery.headers(辞書)を返します。Flask や FastAPI のようなテストクライアントでは、delivery.body を生の本文として、delivery.headers とともに POST してください。

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, ...) は delivery.Body と delivery.Header を返します。delivery.Request(url) は Content-Type: application/json を使って POST リクエストを作成します。オプションは WithWebhookSecret、WithWebhookID および WithWebhookTimestamp です。

キットが行わないこと