
# 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 を作成する](/docs/api/management/apis/create)）。その `identifier`（例: `https://api.example.com`）が、API が受け入れるトークンの audience になります。
- その API に対する `messages:read` スコープ。呼び出し側のアプリケーションに付与されていること。
- Node.js 22 以降。

## 1. SDK をインストールする

```sh
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` から検証済みのクレームを読み取ります。

```js
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 は、最初にトークンを見たときに環境から署名鍵を取得します。

```js
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 を呼び出す

```sh
npm start
```

最初の呼び出しとして、API に対して `messages:read` スコープを持つマシン間アプリケーションの client credentials grant でトークンを取得します。

```sh
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 アカウントも不要です。[オフラインテストキット](/docs/sdk/testing)を参照してください。

```js
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);
});
```

```sh
npm test
```

この例のテストでは、有効なトークンが受け入れられること、そして、期限切れ・別 API 向け・不正な署名・失効したトークン、トークンの不在、スコープ不足、URL クエリ内のトークンが、以下のステータスで拒否されることを確認します。また、`PUBLIC_BASE_URL` 向けの証明を伴う DPoP バインドトークンを送信し、`server. The tests also send signed `token.revoked` deliveries: a valid one revokes the token, a forged, tampered or stale one gets `400` 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 を保護する](/docs/sdk/protect-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` メソッドの背後にある共有ストアに保存してください。[失効キャッシュ](/docs/sdk/protect-api#取り消しキャッシュtokenrevoked)を参照してください。

## 次のステップ

- [ミドルウェアで API を保護する](/docs/sdk/protect-api): Fastify、ステップアップ要件、DPoP、すべてのオプション。
- [トークンの検証](/docs/sdk/token-verification): ミドルウェアの下にある verifier。
- [オフラインテストキット](/docs/sdk/testing)。
