테스트 키트로 오프라인 테스트하기
각 서버 SDK(Go, TypeScript, Python)에는 단위 테스트와 CI를 위한 testing 모듈이 있습니다. 이 모듈은 고정된 테스트 키로 서명한 액세스 토큰과 ID 토큰을 생성하고, 네트워크 접근 없이 바로 그 토큰만 받아들이는 토큰 검증기를 만들며, 샘플 이벤트 페이로드를 제공하고, 공개된 테스트 시크릿으로 웹훅 전송에 서명합니다. 테스트는 KeenAuth 환경, 자격 증명, 인터넷 연결 없이 실행됩니다.
작동 방식
- 고정된 공개 테스트 키. 테스트 토큰은 키 ID
keenauth-test-1를 사용해 ES256으로 서명됩니다. 이 키는 의도적으로 공개되어 있습니다. 비공개 부분은 공개된 레이블에서 유도되며, 공개 키 세트는 모든 SDK에 포함되어 있습니다. 이 키를 다른 어떤 곳에서도 신뢰하지 마십시오. - 존재할 수 없는 발급자. 테스트 토큰에는 발급자
https://test.keenauth.invalid가 들어갑니다..invalid최상위 도메인은 (RFC 2606에 따라) 예약되어 있어 절대 해석되지 않으므로, 실제 환경에서는 이 발급자를 가질 수 없습니다. - 명시적인 테스트 모드. SDK 검증기는 코드에서 테스트 모드를 켠 경우에만 이 발급자를 허용합니다. 일반적인 방식으로 구성된 검증기는
.invalid아래의 모든 발급자를 구성 오류로 거부하므로, 실수로 발급자가 테스트 발급자로 설정되어 있더라도 운영 검증기는 테스트 토큰을 절대 수락하지 않습니다. 테스트 키로 서명되었더라도 실제 발급자를 가진 토큰은 거부됩니다. 실제 환경은 다른 키를 게시하기 때문입니다. - 네트워크 사용 없음. 테스트 모드에서 검증기는 테스트 키를 메모리에서 가져오며 아무것도 가져오지 않습니다. 테스트 발급자만 수락합니다.
테스트 모드가 다른 검사를 약화하지는 않습니다. 만료된 토큰, 잘못된 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 모드를 따릅니다. 즉, 식별자, 상태, 변경된 필드 이름만 포함하고 개인정보는 포함하지 않습니다. 이벤트 핸들러를 테스트할 때 사용하십시오.
- Go:
keenauthtest.Event("user.created")은 JSON을 반환하고,keenauthtest.EventTypes()는 유형 목록을 제공합니다. - TypeScript:
sampleEvent("user.created")과sampleEventTypes(). - Python:
testing.sample_event("user.created")와testing.event_types().
웹훅 테스트하기
키트는 KeenAuth 웹훅 전송과 똑같은 방식(Standard Webhooks v1)으로 페이로드에 서명합니다. 원시 본문과 webhook-id, webhook-timestamp, webhook-signature 헤더를 반환합니다. 핸들러는 운영 환경에서 실행되는 것과 같은 코드인 SDK의 웹훅 검증기로 이를 검증합니다. 다른 점은 시크릿뿐입니다.
- 공개된 테스트 시크릿. 기본적으로 키트는 공개된 레이블
KeenAuth offline test kit webhook secret (public, TEST ONLY)에서 키가 유도된 고정idp_whsec_테스트 시크릿으로 서명합니다. 누구나 이를 계산할 수 있으므로 실제 엔드포인트에는 절대 설정하지 마십시오. 테스트에서는 검증기에 이 시크릿을 설정하거나, 헬퍼에 직접 만든 시크릿을 전달하십시오. - 실제 전송과 동일.
webhook-id는 이벤트의id이며(페이로드에 없으면 무작위msg_ID),webhook-timestamp는 별도로 설정하지 않으면 현재 시각입니다. 본문은 전달한 그대로 전송됩니다. - 재전송과 변조. 오래된 타임스탬프로 서명하여 핸들러가 5분 기간을 벗어난 전송을 거부하는지 테스트하고, 서명 후 본문을 변경하여 잘못된 서명을 거부하는지 테스트하십시오.
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입니다.
키트가 하지 않는 일
- KeenAuth를 에뮬레이션하지 않습니다. 로그인 흐름, 토큰 발급 규칙, 속도 제한, 그 밖의 모든 서버 동작은 호스팅된
development환경에서만 실제로 동작합니다. 출시 전에 해당 환경에서 통합 테스트를 실행하십시오. - 테스트용 시계는 없습니다. 시간을 제어하려면 직접 만든 시계를 검증기(
Now,now)에 전달하고 토큰의 발급 시각을 설정하십시오. - DPoP 증명을 생성하지 않습니다.
cnf.jkt클레임을 추가할 수는 있지만, 그 경우 증명에는 직접 서명해야 합니다. - 웹훅을 전달하지 않습니다. 실제 전송, 재시도, 이벤트 선택은 호스팅된
development환경에서만 실제로 동작합니다.