Django API を保護する
このクイックスタートでは、KeenAuth のアクセストークンで Django API を保護します。Python SDK の @protect ビューデコレーターを追加し、1 つのビューでスコープを必須にし、SDK のテストキットで API をオフラインでテストします。例では Django 5.2 を使用し、データベースは使用しません。
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 が受け付けるトークンのオーディエンスです。 - その API 上の
messages:readスコープ。API を呼び出すアプリケーションに付与されていること。 - Python 3.10 以降。
1. SDK をインストールする
pip install "keenauth-python[django]"
SDK はまだ公開されていません。公開されるまでは、例では SDK のソースからインストールします。例ではすべてのパッケージを requirements.txt 内で正確なバージョンに固定しています。
2. 設定する
project/settings.py はすべての値を環境変数から読み込みます:
| 変数 | 必須 | 意味 |
|---|---|---|
DJANGO_SECRET_KEY |
はい | Django のシークレットキー。生成したうえで、決してコミットしないでください |
DJANGO_ALLOWED_HOSTS |
いいえ | カンマ区切りのホスト名。既定値は localhost,127.0.0.1 |
KEENAUTH_ISSUER |
はい | 環境の発行者 |
KEENAUTH_AUDIENCE |
はい | API の識別子 |
PUBLIC_BASE_URL |
はい | クライアントが呼び出す URL(プロキシ経由での利用を参照) |
KEENAUTH_WEBHOOK_SECRET |
推奨 | idp_whsec_ の署名シークレット(token.revoked を配信する Webhook エンドポイントのもの。ローテーション中はカンマ区切りで 2 つ指定します)。これがないと API は取り消されたトークンを検知できません |
3. ビューを保護する
@protect(protector) は有効なトークンを必須とし、検証済みのクレームを request.auth に格納します。scopes=[...] はさらにスコープも必須にします。チェックに失敗すると、ビューが実行される前に application/problem+json レスポンスを返します。
@require_GET
def public(request: HttpRequest) -> JsonResponse:
# No token needed.
return JsonResponse({"message": "Hello from a public endpoint."})
@require_GET
@protect(protector)
def private(request: HttpRequest) -> JsonResponse:
# Any valid access token for this API; the verified claims are in request.auth.
return JsonResponse({"message": "Hello from a private endpoint.", "sub": request.auth.subject})
@require_GET
@protect(protector, scopes=["messages:read"])
def messages(request: HttpRequest) -> JsonResponse:
# A valid access token that also carries the messages:read scope.
return JsonResponse({"messages": ["Hello from a scoped endpoint."]})
api/auth.py は起動時に設定からプロテクターを構築します(ApiConfig.ready は api/apps.py 内)。そのため、設定が欠けているとリクエストを受ける前にサーバーが停止します。検証器は、最初にトークンを受け取ったときに、環境から署名鍵を取得します。
@cache
def get_protector() -> Protector:
if not settings.KEENAUTH_ISSUER or not settings.KEENAUTH_AUDIENCE or not settings.KEENAUTH_PUBLIC_BASE_URL:
raise ImproperlyConfigured("KEENAUTH_ISSUER, KEENAUTH_AUDIENCE and PUBLIC_BASE_URL are required")
# The verifier refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
verifier = Verifier(VerifierConfig(issuer=settings.KEENAUTH_ISSUER, audience=settings.KEENAUTH_AUDIENCE))
return Protector(
verifier,
public_base_url=settings.KEENAUTH_PUBLIC_BASE_URL,
revocation=revocation,
# The error never contains the token or claim values; never log the Authorization or DPoP header.
on_error=lambda err: log.warning("rejected: %s", err),
)
@cache
def get_webhook() -> Optional[RevocationWebhook]:
"""The token.revoked webhook, or None when KEENAUTH_WEBHOOK_SECRET is not set. A malformed secret raises
WebhookSecretError (without echoing it), so ApiConfig.ready stops the server at startup."""
if not settings.KEENAUTH_WEBHOOK_SECRETS:
return None
return RevocationWebhook(settings.KEENAUTH_WEBHOOK_SECRETS, revocation)
class ProtectorProxy:
"""Hands each request to `get_protector()`, so the `@protect` decorators can be applied at import time."""
def authenticate(self, req: RequestInfo, requirement: Optional[Requirement] = None) -> AccessClaims:
return get_protector().authenticate(req, requirement)
protector = ProtectorProxy()
4. 実行して API を呼び出す
export DJANGO_SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))')"
export KEENAUTH_ISSUER=https://your-environment.eu.keenauth.app
export KEENAUTH_AUDIENCE=https://api.example.com
export PUBLIC_BASE_URL=https://api.example.com
python manage.py runserver 8080
runserver は開発専用です。本番環境では WSGI サーバーの下で project.wsgi を実行してください。
最初の呼び出しには、messages:read スコープを API に持つマシン間(machine-to-machine)アプリケーションの client credentials グラントでトークンを取得します:
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.testing は公開テスト鍵で署名したトークンを発行し、testing.verifier はそれらのトークンのみを受け付けます。テストでは get_protector を、その検証器を使うプロテクターに置き換えるため、ネットワーク、KeenAuth アカウント、設定内のテスト用スイッチのいずれも不要です。オフラインテストキットを参照してください。
class ProtectedAPITest(SimpleTestCase):
def setUp(self) -> None:
self.cache = MemoryRevocationCache()
test_protector = Protector(testing.verifier(audience=AUDIENCE), public_base_url=BASE_URL, revocation=self.cache)
patcher = mock.patch.object(auth, "get_protector", return_value=test_protector)
patcher.start()
self.addCleanup(patcher.stop)
def test_scope(self) -> None:
self.assert_problem(self.get("/api/messages", testing.mint_access_token(AUDIENCE)), 403, "insufficient_scope",
'scope="messages:read"')
tok = testing.mint_access_token(AUDIENCE, claims={"scope": "profile messages:read"})
self.assertEqual(self.get("/api/messages", tok).status_code, 200)
set -a; . ./.env.example; set +a
DJANGO_SECRET_KEY=insecure-test-only python manage.py test
例のテストでは、有効なトークンが受け入れられること、ならびに、トークンがない、期限切れ、対象が別 API、スコープ不足、URL クエリ内のトークン、取り消されたトークンがいずれも以下のステータスで拒否されることを確認します。また、PUBLIC_BASE_URL 向けの証明を伴う DPoP バインドのトークンを送信し、本番用プロテクターがテスト発行者を拒否することも確認します。さらに、署名付きの token.revoked 配信も送信します。有効なものはトークンを取り消し、偽造・改ざん・古いものは 400 を返して何も取り消さず、同じものを繰り返した場合の効果は 1 回だけです。
レスポンス
| リクエスト | ステータス | 本文中の code |
|---|---|---|
Authorization ヘッダーがない |
401 |
missing_token |
| 期限切れのトークン、別 API 向けのトークン、不正な署名、取り消されたトークン | 401 |
invalid_token |
| 必要なスコープがないトークン | 403 |
insufficient_scope |
URL クエリ内のトークン(?access_token=) |
400 |
invalid_request |
本文は application/problem+json で、WWW-Authenticate ヘッダーには標準のチャレンジが入ります。Django は複数のチャレンジを 1 つのヘッダーにまとめます。完全な表は ミドルウェアで API を保護するにあります。
プロキシ経由での利用
クライアントが DPoP を使用する場合、ミドルウェアは各証明がクライアントの呼び出した URL 向けに作成されたことを確認します。PUBLIC_BASE_URL を、クライアントが呼び出す外部 URL(例: https://api.example.com)に設定してください。プロキシやロードバランサーの背後でも同様です。これは必須で、https を使用する必要があります(http は localhost、127.0.0.1、[::1] の場合のみ)。設定がないとアプリは起動を拒否します。ミドルウェア自身は X-Forwarded-* ヘッダーを読み取りません。Django は SECURE_PROXY_SSL_HEADER または USE_X_FORWARDED_HOST を設定した場合にのみそれらを適用し、PUBLIC_BASE_URL を設定すると、確認される URL はそれらに依存しません。
取り消されたトークン
アクセストークンは期限切れまで有効なままなので、KeenAuth はトークンが取り消された際に API に通知します。例では、インメモリの取り消しキャッシュをプロテクターに渡し、それを更新する token.revoked Webhook を POST /webhooks/keenauth(api/webhook.py)で提供します。
- ダッシュボードで、URL
https://<your host>/webhooks/keenauthを持ち、token.revokedを購読する Webhook エンドポイントを作成し、その署名シークレットを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、セレクターの種別、理由をログに記録しますが、ボディ、署名、シークレットは決して記録しません。各ワーカープロセスは独自のキャッシュを持ちます。ワーカーやインスタンスが複数ある場合は、代わりに共有ストアを使用してください。取り消しキャッシュを参照してください。
次のステップ
- ミドルウェアで API を保護する: Flask、ステップアップ要件、DPoP、すべてのオプション。
- トークンを検証する: ミドルウェアの下で動作する検証器。
- オフラインテストキット。