Express API を保護する
このクイックスタートでは、KeenAuth のアクセストークンで Express 5 API を保護します。Node.js SDK の expressAuth ミドルウェアを追加し、1 つのルートにスコープを要求して、SDK のテストキットで API をオフラインでテストします。Express は SDK の依存関係ではありません。このミドルウェアは Express 4 でも動作します。
API のエンドポイントは 3 つあります。
| エンドポイント | 要件 |
|---|---|
GET /api/public |
不要 |
GET /api/private |
API 用の有効なアクセストークン |
GET /api/messages |
messages:read スコープを持つ有効なアクセストークン |
始める前に
- KeenAuth 環境。その発行者(issuer)は
https://your-environment.eu.keenauth.appのような形式です。 - 環境に登録済みの API(API を作成する)。その
identifier(例:https://api.example.com)が、API が受け入れるトークンの audience になります。 - その API に対する
messages:readスコープ。呼び出し側のアプリケーションに付与されていること。 - Node.js 22 以降。
1. SDK をインストールする
npm install @keenauth/node express@5.2.1
SDK はまだ公開されていません。公開されるまでは、この例では package.json 内の SDK ソースを参照しています。
2. 設定する
API は設定を環境変数からのみ読み込みます。それらを .env ファイルに記述してください。npm start は node --env-file-if-exists=.env でそのファイルを読み込みます。このファイルは決してコミットしないでください。
| 変数 | 必須 | 意味 |
|---|---|---|
KEENAUTH_ISSUER |
はい | 環境の発行者 |
KEENAUTH_AUDIENCE |
はい | API の識別子 |
PUBLIC_BASE_URL |
はい | クライアントが呼び出す URL(プロキシ背後での利用を参照) |
KEENAUTH_WEBHOOK_SECRET |
推奨 | idp_whsec_ の署名シークレット。token.revoked を配信する Webhook エンドポイントのもの(ローテーション中はカンマ区切りで 2 つ)。これがないと、API は失効したトークンを知ることができません |
PORT |
いいえ | リッスンするポート。デフォルトは 8080 |
3. ルートを保護する
createApp がプロテクタとルートを作成します。expressAuth(protector) は有効なトークンを要求します。{ scopes } を指定すると、スコープも要求します。ハンドラは req.auth から検証済みのクレームを読み取ります。
export function createApp({ verifier, publicBaseUrl, revocation, webhookSecrets = [] }) {
const protector = createProtector(verifier, {
publicBaseUrl,
revocation,
// The error never contains the token or claim values; never log the Authorization or DPoP header.
onError: (err) => console.warn(`rejected: ${err.message}`),
});
const app = express();
app.disable("x-powered-by");
// No token needed.
app.get("/api/public", (req, res) => {
res.json({ message: "Hello from a public endpoint." });
});
// Any valid access token for this API.
app.get("/api/private", expressAuth(protector), (req, res) => {
res.json({ message: "Hello from a private endpoint.", sub: req.auth.subject });
});
// A valid access token that also carries the messages:read scope.
app.get("/api/messages", expressAuth(protector, { scopes: ["messages:read"] }), (req, res) => {
res.json({ messages: ["Hello from a scoped endpoint."] });
});
// token.revoked deliveries from KeenAuth; the handler reads the raw body itself, so no JSON parser runs before it.
if (webhookSecrets.length > 0) {
app.post(WEBHOOK_PATH, ...createWebhookHandler({ secrets: webhookSecrets, revocation }));
} else {
console.warn("KEENAUTH_WEBHOOK_SECRET is not set: token.revoked events are not received, so revoked tokens stay valid until they expire");
}
return app;
}
server.js は設定から実際の verifier を構築します。verifier は、最初にトークンを見たときに環境から署名鍵を取得します。
const issuer = process.env.KEENAUTH_ISSUER;
const audience = process.env.KEENAUTH_AUDIENCE;
// The URL clients call: DPoP proofs are checked against it, never against the Host header.
const publicBaseUrl = process.env.PUBLIC_BASE_URL;
if (!issuer || !audience || !publicBaseUrl) {
console.error("KEENAUTH_ISSUER, KEENAUTH_AUDIENCE and PUBLIC_BASE_URL are required");
process.exit(1);
}
// createVerifier refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
const verifier = createVerifier({ issuer, audience });
const revocation = createMemoryRevocationCache();
// The signing secret (idp_whsec_...) of the webhook endpoint that delivers token.revoked; comma separated while rotating.
const webhookSecrets = (process.env.KEENAUTH_WEBHOOK_SECRET ?? "").split(",").map((s) => s.trim()).filter(Boolean);
const app = createApp({ verifier, publicBaseUrl, revocation, webhookSecrets });
const port = Number(process.env.PORT || 8080);
app.listen(port, () => console.log(`listening on port ${port}`));
4. 実行して API を呼び出す
npm start
最初の呼び出しとして、API に対して messages:read スコープを持つマシン間アプリケーションの client credentials grant でトークンを取得します。
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials \
-d resource=https://api.example.com \
-d scope=messages:read \
https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)
curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
5. ネットワークなしでテストする
@keenauth/node/testing は公開テスト鍵で署名されたトークンを発行し、createTestVerifier はそれらのトークンのみを受け付けます。テストでは、この verifier を createApp に渡し、ローカルポートでアプリを呼び出します。そのためネットワークも KeenAuth アカウントも不要です。オフラインテストキットを参照してください。
test("scope check: 403 insufficient_scope without messages:read, 200 with it", async () => {
await expectProblem(await get("/api/messages", bearer(mintAccessToken(AUDIENCE))), 403, "insufficient_scope", /scope="messages:read"/);
const ok = await get("/api/messages", bearer(mintAccessToken(AUDIENCE, { claims: { scope: "profile messages:read" } })));
assert.equal(ok.status, 200);
});
npm test
この例のテストでは、有効なトークンが受け入れられること、そして、期限切れ・別 API 向け・不正な署名・失効したトークン、トークンの不在、スコープ不足、URL クエリ内のトークンが、以下のステータスで拒否されることを確認します。また、PUBLIC_BASE_URL 向けの証明を伴う DPoP バインドトークンを送信し、server. The tests also send signed token.revokeddeliveries: a valid one revokes the token, a forged, tampered or stale one gets400 and revokes nothing, and a repeated one has one effect.js が、テスト発行者では起動を拒否することも確認します。
レスポンス
| リクエスト | ステータス | 本文の code |
|---|---|---|
Authorization ヘッダーがない |
401 |
missing_token |
| 期限切れ、別 API 向けのトークン、不正な署名、失効したトークン | 401 |
invalid_token |
| 必要なスコープがないトークン | 403 |
insufficient_scope |
URL クエリ内のトークン(?access_token=) |
400 |
invalid_request |
本文は application/problem+json で、WWW-Authenticate ヘッダーに標準のチャレンジが含まれます。完全な表はミドルウェアで API を保護するにあります。
プロキシ背後での利用
クライアントが DPoP を使用する場合、ミドルウェアは各証明が、クライアントの呼び出した URL に対して作成されたものかを確認します。PUBLIC_BASE_URL を、クライアントが呼び出す外部 URL(例: https://api.example.com)に設定してください。プロキシやロードバランサの背後でも同様です。これは必須で、https を使う必要があります(http は localhost、127.0.0.1、[::1] の場合のみ)。これがないとアプリは起動を拒否します。Express の trust proxy の設定がどうであっても、ミドルウェアは X-Forwarded-* ヘッダーを決して信頼しません。
失効したトークン
アクセストークンは期限切れまで有効なままなので、KeenAuth はトークンが失効したことを API に伝えます。この例では、プロテクタにインメモリの失効キャッシュを渡し、それにデータを供給する token.revoked Webhook を POST /webhooks/keenauth(webhook.js)で提供します。
- ダッシュボードで、URL
https://<your host>/webhooks/keenauthを指定した Webhook エンドポイントを作成し、token.revokedを購読させ、その署名シークレットをKEENAUTH_WEBHOOK_SECRETとしてエクスポートします。 - ハンドラは生のボディに対して SDK で署名を検証し、署名が無効な場合、またはタイムスタンプが 5 分以上ずれている場合は
400を返します。イベントのセレクタ(access_token,session,user,client,org,org_user,client_user)を失効マーカーに対応付け、204を返します。その後、ミドルウェアはnot_beforeより前に発行された一致するトークンをすべて拒否します。 - KeenAuth は少なくとも 1 回配信するため、ハンドラは各
webhook-idを 1 回だけ適用します。マーカーを保存できない場合は503を返し、KeenAuth はイベントを再配信します。他のイベントタイプや、どのアクセストークンにも一致しないセレクタ(refresh_family,api)は無視します。
ハンドラは、イベント ID、セレクタの種類、理由をログに記録しますが、ボディ、署名、シークレットは決してログに記録しません。各プロセスは独自のキャッシュを持ちます。複数インスタンスで運用する場合は、マーカーを同じ revoke メソッドの背後にある共有ストアに保存してください。失効キャッシュを参照してください。
次のステップ
- ミドルウェアで API を保護する: Fastify、ステップアップ要件、DPoP、すべてのオプション。
- トークンの検証: ミドルウェアの下にある verifier。
- オフラインテストキット。