
# ミドルウェアで API を保護する

各サーバー SDK には、KeenAuth のアクセストークンで API を保護する薄いミドルウェアが含まれています。Go では `net/http`、Node.js では Express と Fastify、Python では Django、Flask、FastAPI に対応しています。ミドルウェアは [トークン検証機能](/docs/sdk/token-verification) の上に構築されており、独自のトークン処理は行いません。

## ミドルウェアの動作

- **トークンを読み取ります。** `Authorization` ヘッダーから `Bearer` または `DPoP` スキームのトークンを読み取ります。URL のクエリ（`?access_token=`）に含まれるトークンは、URL がログや `Referer` ヘッダーに残るため、常に拒否されます。`Authorization` ヘッダーが複数ある場合や、資格情報が複数ある場合も拒否されます。
- **DPoP を検証します。** `DPoP` スキームの場合、`DPoP` 証明ヘッダー、リクエストメソッド、絶対リクエスト URL（設定された公開ベース URL にリクエストパスを続けたもの）が検証機能に渡されます。DPoP にバインドされたトークンを `Bearer` スキームで送信した場合は拒否されます。DPoP にバインドされていないトークンを `DPoP` スキームで送信した場合も拒否されます。
- **ルートの要件を確認します。** 必要なスコープ（すべて）を確認します。必要に応じて、ステップアップ認証も確認します。許容する `acr` 値や、サインインからの最大経過時間（RFC 9470）を指定できます。
- **取り消しキャッシュに確認します。** 取り消しキャッシュを設定している場合は、トークンが取り消されているかどうかを確認します。
- **検証済みのクレームをリクエストに添付します。** Go では `protect.ClaimsFrom(ctx)`、Express と Fastify では `req.auth`、Django では `request.auth`、Flask では `g.auth`、FastAPI では依存関係の戻り値として取得できます。
- **失敗時は応答します。** 標準の `WWW-Authenticate` チャレンジと、小さな `application/problem+json` 形式の本文を返します。応答にトークンやクレームの値が含まれることはなく、ミドルウェアがそれらをログに記録することもありません。

## Go（net/http）

パッケージ `protect` は標準ライブラリのみを使用し、`http.Handler` を受け付けるあらゆるルーターで動作します。

```go
import (
	"github.com/keenauth/keenauth-go/protect"
	"github.com/keenauth/keenauth-go/verify"
)

v, err := verify.New(verify.Config{
	Issuer:   "https://acme.eu.keenauth.app",
	Audience: "https://api.acme.example",
})
if err != nil {
	return err
}
m, err := protect.New(v, protect.Options{
	PublicBaseURL:       "https://api.acme.example",
	ResourceMetadataURL: "https://api.acme.example/.well-known/oauth-protected-resource",
})
if err != nil {
	return err
}

mux := http.NewServeMux()
mux.Handle("GET /orders", m.Require(protect.Requirement{Scopes: []string{"orders:read"}})(http.HandlerFunc(
	func(w http.ResponseWriter, r *http.Request) {
		claims, _ := protect.ClaimsFrom(r.Context())
		fmt.Fprintln(w, claims.Subject)
	})))

age := 10 * time.Minute
mux.Handle("POST /payouts", m.Require(protect.Requirement{
	Scopes:        []string{"payouts:write"},
	AcceptableACR: []string{"urn:idp:acr:mfa"},
	MaxAuthAge:    &age,
})(payoutsHandler))
```

`m.Handler(next)` は、有効なトークンのみを要求し、それ以上の要件がないハンドラーを保護します。`http.ServeMux` が不正なパターンに対して行うのと同様に、スコープが有効なスコープトークンでない場合、`Require` は起動時にパニックを起こします。

## Node.js（Express と Fastify）

ミドルウェアは `@keenauth/node/middleware` のサブパスにあります。Express と Fastify は SDK の依存関係には含まれていません。

```ts
import { createVerifier } from "@keenauth/node";
import { createProtector, expressAuth, fastifyAuth } from "@keenauth/node/middleware";

const verifier = createVerifier({ issuer: "https://acme.eu.keenauth.app", audience: "https://api.acme.example" });
const protector = createProtector(verifier, { publicBaseUrl: "https://api.acme.example" });

// Express
app.get("/orders", expressAuth(protector, { scopes: ["orders:read"] }), (req, res) => {
  res.json({ sub: req.auth.subject });
});

// Fastify
fastify.decorateRequest("auth", null);
fastify.get("/orders", { preHandler: fastifyAuth(protector, { scopes: ["orders:read"] }) }, async (request) => {
  return { sub: request.auth.subject };
});
```

ステップアップ認証では `{ acceptableAcr: ["urn:idp:acr:mfa"], maxAuthAgeSeconds: 600 }` を使用します。TypeScript では、`req.auth` を読み取るために、リクエストの型をサブパスからエクスポートされる `AuthenticatedRequest` として指定してください。

## Python（Django、Flask、FastAPI）

使用するフレームワーク向けのエクストラをインストールしてください。`pip install "keenauth-python[django]"`、`"keenauth-python[flask]"`、または `"keenauth-python[fastapi]"` を実行します。フレームワークは、それぞれ専用のアダプターモジュールからのみインポートされます。

```python
from keenauth.middleware import Protector
from keenauth.verify import Verifier, VerifierConfig

verifier = Verifier(VerifierConfig(issuer="https://acme.eu.keenauth.app", audience="https://api.acme.example"))
protector = Protector(verifier, public_base_url="https://api.acme.example")
```

Django の場合:

```python
from django.http import JsonResponse
from keenauth.middleware.django import protect

@protect(protector, scopes=["orders:read"])
def orders(request):
    return JsonResponse({"sub": request.auth.subject})
```

Flask の場合:

```python
from flask import g
from keenauth.middleware.flask import protect

@app.get("/orders")
@protect(protector, scopes=["orders:read"])
def orders():
    return {"sub": g.auth.subject}
```

FastAPI の場合:

```python
from fastapi import Depends, FastAPI
from keenauth.middleware.fastapi import RequireAuth, install
from keenauth.verify import AccessClaims

app = FastAPI()
install(app)

@app.get("/orders")
def orders(claims: AccessClaims = Depends(RequireAuth(protector, scopes=["orders:read"]))):
    return {"sub": claims.subject}
```

`install(app)` は、問題の本文と、チャレンジごとに 1 つの `WWW-Authenticate` ヘッダーを書き出すハンドラーを登録します。ステップアップ認証では `acceptable_acr=["urn:idp:acr:mfa"], max_auth_age=timedelta(minutes=10)` を使用します。

## 公開ベース URL（必須）

DPoP 証明には、クライアントが呼び出した URL（`htu`）が含まれています。ミドルウェアは、この URL を、設定した公開ベース URL（`PublicBaseURL`、`publicBaseUrl`、`public_base_url`）にリクエストパスを続けたものと比較します。クライアントが使用するアドレス（例: `https://api.acme.example` や `https://acme.example/api`）を設定してください。この設定は必須です。クライアントが Bearer トークンのみを送信する場合でも、これを指定せずにミドルウェアを作成すると、起動時に設定エラーで失敗します。

ミドルウェアは、この URL をリクエストの `Host` ヘッダーや `X-Forwarded-*` ヘッダーから組み立てることはありません。Python では、フレームワークやサーバーが転送ヘッダーを信頼するよう設定されていても、フレームワーク自身のリクエスト URL は使用しません。値はクライアントが決めるため、API が 2 つのホスト名で提供されている場合、一方のホスト名向けに作成された証明が、もう一方のホスト名に対して再利用されるおそれがあります。ロードバランサーやリバースプロキシの背後に配置する場合、公開ベース URL は、クライアントに見えるスキーム、ホスト、パスプレフィックスをミドルウェアに伝える役割も果たします。公開ベース URL は、`localhost`、`127.0.0.1`、`[::1]` を除いて `https` を使用する必要があります（RFC 6750 §5.3）。スキームとホストは大文字と小文字を区別せずに比較され、既定のポート（`:443`、`:80`）は無視されます。これは、ベース URL と証明の `htu` の両方に適用されます。

## 応答

| 状況 | ステータス | `WWW-Authenticate` | `code` |
|---|---|---|---|
| トークンがない、またはサポートされていないスキーム | `401` | `Bearer` と `DPoP algs="…"`。エラーは含まない | `missing_token` |
| クエリ内のトークン、重複している、または不正な形式の `Authorization` | `400` | `Bearer error="invalid_request"` | `invalid_request` |
| 無効、期限切れ、または取り消し済みのトークン | `401` | `error="invalid_token"` | `invalid_token` |
| DPoP 証明がない、重複している、または無効 | `401` | `DPoP error="invalid_dpop_proof"` | `invalid_dpop_proof` |
| 必要なスコープがない | `403` | `error="insufficient_scope", scope="…"` | `insufficient_scope` |
| ステップアップ認証が必要 | `401` | `error="insufficient_user_authentication", acr_values="…", max_age="…"` | `insufficient_user_authentication` |
| 署名鍵または取り消し状態を利用できない | `503` | なし | `jwks_unavailable`、`revocation_unavailable` |

チャレンジでは、クライアントが使用したスキームが使われます。リソースメタデータ URL を設定すると、すべての `401` チャレンジに `resource_metadata`（RFC 9728）が含まれます。本文には `type`、`title`、`status`、`code`、および固定の `detail` が含まれます。トークンが無効だった理由は本文に示されません。ログに記録するには、エラーコールバック（`OnError`、`onError`、`on_error`）を渡してください。このコールバックには、トークンやクレームの値を含まない検証機能のエラーが渡されます。`Authorization` ヘッダーや `DPoP` ヘッダーを、独自にログへ記録しないでください。

## 取り消しキャッシュ（`token.revoked`）

JWT のアクセストークンは、期限切れになるまで有効です。取り消されたトークンをより早く拒否するには、`token.revoked` Webhook を処理し、そのセレクターを、ミドルウェアが参照する取り消しキャッシュに記録してください。SDK にはメモリ内キャッシュが含まれています。Go では `protect.NewMemoryRevocationCache`、Node.js では `createMemoryRevocationCache`、Python では `MemoryRevocationCache` を使用します。

セレクターは、トークン ID（`jti`）、セッション（`sid`）、ユーザー（`sub`）、組織（`org_id`）、アプリケーション（`client_id`）、または組織とユーザーの組み合わせ、アプリケーションとユーザーの組み合わせを指定します。`jti` セレクターは、そのトークンを取り消します。その他のセレクターは、イベントの `not_before` より前に発行されたすべてのトークンを取り消します。それ以降に発行されたトークンは受け入れられます。マーカーは、既定で 24 時間と 1 分（最長のアクセストークン有効期間にクロックスキューを加えた時間）保持されます。

```go
cache := protect.NewMemoryRevocationCache(0, nil)
m, _ := protect.New(v, protect.Options{PublicBaseURL: "https://api.acme.example", Revocation: cache})

// 検証済みの token.revoked Webhook ハンドラー内:
err := cache.Revoke(protect.Selector{SessionID: "ses_…"}, notBefore)
```

メモリ内キャッシュは 1 つのプロセスに属します。API を複数のインスタンスで実行する場合は、共有ストア上に取り消しチェッカーのインターフェースを実装してください。キャッシュが応答できない場合、ミドルウェアはリクエストを `503` で拒否します。

## テスト

ミドルウェアのテストでトークンを発行するには、[オフラインテストキット](/docs/sdk/testing) を使用してください。このキットには DPoP 証明のヘルパーは含まれていません。テストでは、使用している言語の暗号ライブラリを使って証明に署名してください。
