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 スコープを持つ有効なアクセストークン

始める前に

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)で提供します。

  1. ダッシュボードで、URL https://<your host>/webhooks/keenauth を指定した Webhook エンドポイントを作成し、token.revoked を購読させ、その署名シークレットを KEENAUTH_WEBHOOK_SECRET としてエクスポートします。
  2. ハンドラは生のボディに対して SDK で署名を検証し、署名が無効な場合、またはタイムスタンプが 5 分以上ずれている場合は 400 を返します。イベントのセレクタ(access_token, session, user, client, org, org_user, client_user)を失効マーカーに対応付け、204 を返します。その後、ミドルウェアは not_before より前に発行された一致するトークンをすべて拒否します。
  3. KeenAuth は少なくとも 1 回配信するため、ハンドラは各 webhook-id を 1 回だけ適用します。マーカーを保存できない場合は 503 を返し、KeenAuth はイベントを再配信します。他のイベントタイプや、どのアクセストークンにも一致しないセレクタ(refresh_family, api)は無視します。

ハンドラは、イベント ID、セレクタの種類、理由をログに記録しますが、ボディ、署名、シークレットは決してログに記録しません。各プロセスは独自のキャッシュを持ちます。複数インスタンスで運用する場合は、マーカーを同じ revoke メソッドの背後にある共有ストアに保存してください。失効キャッシュを参照してください。

次のステップ