<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>OAuth2 on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/oauth2/</link>
    <description>Recent content in OAuth2 on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Wed, 04 Mar 2026 09:35:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/oauth2/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>FastAPI認証・認可の本番設計：JWT運用、権限制御、監査ログまで含めた実装パターン</title>
      <link>https://www.ai2core.com/posts/2026-03-04-fastapi-authn-authz-production-patterns/</link>
      <pubDate>Wed, 04 Mar 2026 09:35:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-fastapi-authn-authz-production-patterns/</guid>
      <description>FastAPIで安全に認証・認可を実装するために、トークン設計、ローテーション、RBAC、監査、障害時運用まで具体手順で解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi認証認可の本番設計jwt運用権限制御監査ログまで含めた実装パターン">FastAPI認証・認可の本番設計：JWT運用、権限制御、監査ログまで含めた実装パターン</h1>
<p>FastAPI は実装が速い反面、認証・認可を最小構成のまま本番に出してしまい、後からセキュリティ事故に発展するケースが少なくありません。特に「JWT を入れたから安全」という誤解は危険です。</p>
<p>本記事では、<strong>開発速度を落とさずに本番で耐える認証基盤</strong>を作るための設計を、コード例と運用手順込みで解説します。</p>
<h2 id="1-認証と認可を分離して設計する">1. 認証と認可を分離して設計する</h2>
<p>最初に押さえるべきは責務分離です。</p>
<ul>
<li>認証（Authentication）: 誰かを確認する</li>
<li>認可（Authorization）: 何をしてよいか判定する</li>
</ul>
<p>この2つを混ぜると、実装も監査も破綻します。FastAPI では dependency を分け、<code>get_current_user</code> と <code>require_permission</code> を独立させるのが基本です。</p>
<h2 id="2-jwt-は短命--リフレッシュ--失効管理で使う">2. JWT は「短命 + リフレッシュ + 失効管理」で使う</h2>
<p>アクセストークンを長寿命にすると、漏えい時の被害が大きくなります。実運用では以下が標準です。</p>
<ul>
<li>Access Token: 5〜15分</li>
<li>Refresh Token: 7〜30日</li>
<li>Refresh Token は DB 保存し、ローテーション時に旧トークンを失効</li>
</ul>
<p><code>sub</code> だけでなく、<code>jti</code>（トークンID）や <code>scope</code> を持たせると管理しやすくなります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime, timedelta, timezone
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> jwt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ALGORITHM <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HS256&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create_access_token</span>(user_id: str, scopes: list[str], secret: str) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>    now <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now(timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>    payload <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;sub&#34;</span>: user_id,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;scope&#34;</span>: <span style="color:#e6db74">&#34; &#34;</span><span style="color:#f92672">.</span>join(scopes),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iat&#34;</span>: int(now<span style="color:#f92672">.</span>timestamp()),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;exp&#34;</span>: int((now <span style="color:#f92672">+</span> timedelta(minutes<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>))<span style="color:#f92672">.</span>timestamp()),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;jti&#34;</span>: <span style="color:#e6db74">&#34;generated-uuid&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> jwt<span style="color:#f92672">.</span>encode(payload, secret, algorithm<span style="color:#f92672">=</span>ALGORITHM)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="3-鍵管理とローテーション">3. 鍵管理とローテーション</h2>
<p>秘密鍵を <code>.env</code> に固定して数年運用するのは典型的な事故パターンです。最低限、次を実施します。</p>
<ul>
<li>KMS/Vault など外部シークレット管理を利用</li>
<li><code>kid</code> をヘッダに持たせ、複数鍵を並行運用</li>
<li>鍵ローテーション手順を runbook 化</li>
</ul>
<p>ローテーションの要点:</p>
<ol>
<li>新鍵を追加（検証側は新旧どちらも受理）</li>
<li>発行側を新鍵へ切替</li>
<li>旧鍵の有効期限を過ぎたら削除</li>
</ol>
<p>この手順にすると、無停止で切替できます。</p>
<h2 id="4-fastapi-dependencyで認可を明示化">4. FastAPI Dependencyで認可を明示化</h2>
<p>ロジック中で <code>if user.role == &quot;admin&quot;</code> を乱立させると、抜け漏れが起こります。権限チェックは dependency 化し、ルート定義に明示します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> Depends, HTTPException, status
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">require_permission</span>(required: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">checker</span>(user<span style="color:#f92672">=</span>Depends(get_current_user)):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> required <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> user<span style="color:#f92672">.</span>permissions:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> HTTPException(
</span></span><span style="display:flex;"><span>                status_code<span style="color:#f92672">=</span>status<span style="color:#f92672">.</span>HTTP_403_FORBIDDEN,
</span></span><span style="display:flex;"><span>                detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;insufficient permissions&#34;</span>
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> user
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> checker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@router.delete</span>(<span style="color:#e6db74">&#34;/projects/</span><span style="color:#e6db74">{project_id}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">delete_project</span>(
</span></span><span style="display:flex;"><span>    project_id: str,
</span></span><span style="display:flex;"><span>    user<span style="color:#f92672">=</span>Depends(require_permission(<span style="color:#e6db74">&#34;project:delete&#34;</span>))
</span></span><span style="display:flex;"><span>):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ルート単位で要件が見えるため、レビュー効率と監査性が上がります。</p>
<h2 id="5-rbacabac-の使い分け">5. RBAC/ABAC の使い分け</h2>
<p>小規模なら RBAC（role-based）で十分ですが、顧客単位データや組織階層があると ABAC（属性ベース）を併用した方が安全です。</p>
<ul>
<li>RBAC: <code>admin</code>, <code>editor</code>, <code>viewer</code></li>
<li>ABAC: <code>tenant_id</code>, <code>resource_owner_id</code>, <code>department</code></li>
</ul>
<p>実務では「ロールで粗く許可し、属性で絞る」が扱いやすいです。</p>
<h2 id="6-マルチテナントで必須の防御">6. マルチテナントで必須の防御</h2>
<p>マルチテナント API では、ID 推測よりも<strong>テナント境界漏れ</strong>が主要リスクです。対策は次の通りです。</p>
<ul>
<li>すべての DB クエリに <code>tenant_id</code> 条件を必須化</li>
<li>管理者 API でも境界を明示的に超える操作だけ許可</li>
<li>監査ログに <code>tenant_id</code>, <code>actor_id</code>, <code>resource_id</code> を残す</li>
</ul>
<p>SQLAlchemy でも repository 層で共通フィルタを強制すると漏れを減らせます。</p>
<h2 id="7-監査ログを設計段階で入れる">7. 監査ログを設計段階で入れる</h2>
<p>認証系は障害後に「誰が何をしたか」が必要になります。後付けだと間に合いません。最低限、次を記録します。</p>
<ul>
<li>ログイン成功/失敗（IP, user-agent, reason）</li>
<li>権限エラー（403）</li>
<li>重要操作（削除、権限変更、請求情報更新）</li>
<li>トークン失効・再発行</li>
</ul>
<p>フォーマットは JSON 構造化に統一し、SIEM や OpenSearch に流せる形にしておくと分析が速いです。</p>
<h2 id="8-レート制限とブルートフォース対策">8. レート制限とブルートフォース対策</h2>
<p>パスワード認証がある場合、レート制限なしは危険です。<code>slowapi</code> などを使い、ログイン系エンドポイントに制限を入れます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> slowapi <span style="color:#f92672">import</span> Limiter
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> slowapi.util <span style="color:#f92672">import</span> get_remote_address
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>limiter <span style="color:#f92672">=</span> Limiter(key_func<span style="color:#f92672">=</span>get_remote_address)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@router.post</span>(<span style="color:#e6db74">&#34;/auth/login&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@limiter.limit</span>(<span style="color:#e6db74">&#34;5/minute&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">login</span>(<span style="color:#f92672">...</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>さらに次を組み合わせると強化できます。</p>
<ul>
<li>失敗回数に応じた遅延（progressive delay）</li>
<li>CAPTCHA（必要時のみ）</li>
<li>異常IP/ASN の遮断</li>
</ul>
<h2 id="9-よくある実装ミス">9. よくある実装ミス</h2>
<h3 id="ミスa-署名検証はしているが-audiss-未検証">ミスA: 署名検証はしているが <code>aud/iss</code> 未検証</h3>
<p>結果:</p>
<ul>
<li>他システム向けトークンを誤受理</li>
</ul>
<p>対処:</p>
<ul>
<li>issuer/audience を厳格検証</li>
<li>想定外クレームは拒否</li>
</ul>
<h3 id="ミスb-refresh-token-の使い回しを検知しない">ミスB: refresh token の使い回しを検知しない</h3>
<p>結果:</p>
<ul>
<li>漏えい時に長期間乗っ取られる</li>
</ul>
<p>対処:</p>
<ul>
<li>ローテーション時に旧トークン失効</li>
<li>再利用検知時はセッション全失効</li>
</ul>
<h3 id="ミスc-認可チェックが一部エンドポイントで抜ける">ミスC: 認可チェックが一部エンドポイントで抜ける</h3>
<p>結果:</p>
<ul>
<li>水平権限昇格</li>
</ul>
<p>対処:</p>
<ul>
<li>dependency ベースで強制</li>
<li>重要ルートにセキュリティテスト追加</li>
</ul>
<h2 id="10-テスト戦略必須">10. テスト戦略（必須）</h2>
<p>認証はユニットテストだけでなく、統合テストで権限境界を確認します。</p>
<ul>
<li>有効トークン/期限切れ/改ざんトークン</li>
<li>role ごとのアクセス可否</li>
<li>tenant 越境アクセス拒否</li>
<li>refresh token 再利用検知</li>
</ul>
<p>pytest では fixture で role 別トークンを用意し、回帰を防ぎます。</p>
<h2 id="11-障害時-runbook最低限">11. 障害時 runbook（最低限）</h2>
<p>インシデント時に迷わないよう、次を文書化しておきます。</p>
<ol>
<li>鍵漏えい疑い時の全トークン失効手順</li>
<li>認証基盤障害時のフェイル動作（許可しすぎを防ぐ）</li>
<li>監査ログの検索手順</li>
<li>関係者通知テンプレート</li>
</ol>
<p>特に「認証サーバーが落ちたとき、API をどうするか」は事前に決めておく必要があります。</p>
<h2 id="12-導入チェックリスト">12. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> access token は短寿命（&lt;=15分）</li>
<li><input disabled="" type="checkbox"> refresh token は DB 管理 + ローテーション</li>
<li><input disabled="" type="checkbox"> 鍵ローテーション手順がある</li>
<li><input disabled="" type="checkbox"> ルート単位で認可 dependency が明示されている</li>
<li><input disabled="" type="checkbox"> tenant 境界を DB レイヤーで強制している</li>
<li><input disabled="" type="checkbox"> 監査ログ（認証/認可/重要操作）を構造化保存</li>
<li><input disabled="" type="checkbox"> レート制限と異常検知がある</li>
<li><input disabled="" type="checkbox"> 権限境界の統合テストがある</li>
</ul>
<p>FastAPI の認証・認可は、フレームワーク機能だけでは守り切れません。<strong>トークン寿命設計、鍵運用、境界強制、監査、テスト、runbook</strong>まで含めて初めて、本番で信頼できるセキュリティ基盤になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Security</category>
      <category>JWT</category>
      <category>OAuth2</category>
      <category>Python</category>
    </item>
  </channel>
</rss>
