테스트 키트로 오프라인 테스트하기

각 서버 SDK(Go, TypeScript, Python)에는 단위 테스트와 CI를 위한 testing 모듈이 있습니다. 이 모듈은 고정된 테스트 키로 서명한 액세스 토큰과 ID 토큰을 생성하고, 네트워크 접근 없이 바로 그 토큰만 받아들이는 토큰 검증기를 만들며, 샘플 이벤트 페이로드를 제공하고, 공개된 테스트 시크릿으로 웹훅 전송에 서명합니다. 테스트는 KeenAuth 환경, 자격 증명, 인터넷 연결 없이 실행됩니다.

작동 방식

테스트 모드가 다른 검사를 약화하지는 않습니다. 만료된 토큰, 잘못된 audience, 변경된 페이로드, 누락된 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 전달한 audience 전달한 클라이언트 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 모드를 따릅니다. 즉, 식별자, 상태, 변경된 필드 이름만 포함하고 개인정보는 포함하지 않습니다. 이벤트 핸들러를 테스트할 때 사용하십시오.

웹훅 테스트하기

키트는 KeenAuth 웹훅 전송과 똑같은 방식(Standard Webhooks v1)으로 페이로드에 서명합니다. 원시 본문과 webhook-id, webhook-timestamp, webhook-signature 헤더를 반환합니다. 핸들러는 운영 환경에서 실행되는 것과 같은 코드인 SDK의 웹훅 검증기로 이를 검증합니다. 다른 점은 시크릿뿐입니다.

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입니다.

키트가 하지 않는 일