ミドルウェアで API を保護する
各サーバー SDK には、KeenAuth のアクセストークンで API を保護する薄いミドルウェアが含まれています。Go では net/http、Node.js では Express と Fastify、Python では Django、Flask、FastAPI に対応しています。ミドルウェアは トークン検証機能 の上に構築されており、独自のトークン処理は行いません。
ミドルウェアの動作
- トークンを読み取ります。
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 を受け付けるあらゆるルーターで動作します。
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 の依存関係には含まれていません。
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]" を実行します。フレームワークは、それぞれ専用のアダプターモジュールからのみインポートされます。
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 の場合:
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 の場合:
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 の場合:
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 分(最長のアクセストークン有効期間にクロックスキューを加えた時間)保持されます。
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 で拒否します。
テスト
ミドルウェアのテストでトークンを発行するには、オフラインテストキット を使用してください。このキットには DPoP 証明のヘルパーは含まれていません。テストでは、使用している言語の暗号ライブラリを使って証明に署名してください。