mirror of
https://github.com/kennethreitz/responder.git
synced 2026-10-24 05:59:12 +00:00
602 lines
66 KiB
HTML
602 lines
66 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<html lang="en" data-content_root="./">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
|
||
<title>Authentication — responder 9.2.0 documentation</title>
|
||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=5ecbeea2" />
|
||
<link rel="stylesheet" type="text/css" href="_static/basic.css?v=b08954a9" />
|
||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css?v=eb4102c5" />
|
||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||
<link rel="stylesheet" type="text/css" href="_static/sphinx-design.min.css?v=95c83b7e" />
|
||
<link rel="stylesheet" type="text/css" href="_static/design-elements.e5416f61bae5d36adc6d722a2b6f8cff.css?v=452a8e97" />
|
||
<script src="_static/documentation_options.js?v=e8b940be"></script>
|
||
<script src="_static/doctools.js?v=fd6eb6e6"></script>
|
||
<script src="_static/sphinx_highlight.js?v=6ffebe34"></script>
|
||
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
|
||
<script src="_static/copybutton.js?v=fd10adb8"></script>
|
||
<script>
|
||
</script>
|
||
<script src="_static/design-tabs.js?v=f930bc37"></script>
|
||
<script src="_static/design-elements.bbdccc18c4abea9397628f9fea3d48c2.js?v=03c7770e"></script>
|
||
<link rel="index" title="Index" href="genindex.html" />
|
||
<link rel="search" title="Search" href="search.html" />
|
||
<link rel="next" title="WebSocket Tutorial" href="tutorial-websockets.html" />
|
||
<link rel="prev" title="Using SQLAlchemy" href="tutorial-sqlalchemy.html" />
|
||
|
||
<link rel="stylesheet" href="_static/custom.css" type="text/css" />
|
||
|
||
|
||
|
||
|
||
|
||
</head><body>
|
||
|
||
|
||
<div class="document">
|
||
<div class="documentwrapper">
|
||
<div class="bodywrapper">
|
||
|
||
|
||
<div class="body" role="main">
|
||
|
||
<section id="authentication">
|
||
<h1>Authentication<a class="headerlink" href="#authentication" title="Link to this heading">¶</a></h1>
|
||
<p>Every API that handles user data needs authentication — a way to verify
|
||
who is making a request. This guide covers the most common patterns:
|
||
API keys, JWT tokens, and how to build reusable auth guards with
|
||
Responder’s before-request hooks.</p>
|
||
<section id="api-key-authentication">
|
||
<h2>API Key Authentication<a class="headerlink" href="#api-key-authentication" title="Link to this heading">¶</a></h2>
|
||
<p>The simplest approach. The client sends a secret key in a header, and
|
||
your server checks it against a known value. This is common for
|
||
server-to-server communication and simple APIs:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">API_KEYS</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"sk-abc123"</span><span class="p">,</span> <span class="s2">"sk-def456"</span><span class="p">}</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="n">before_request</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">check_api_key</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">key</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"X-API-Key"</span><span class="p">)</span>
|
||
<span class="k">if</span> <span class="n">key</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">API_KEYS</span><span class="p">:</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">401</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"error"</span><span class="p">:</span> <span class="s2">"Invalid or missing API key"</span><span class="p">}</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Because the before-request hook sets <code class="docutils literal notranslate"><span class="pre">resp.status_code</span></code>, the route
|
||
handler is skipped entirely for unauthorized requests. The client never
|
||
reaches your endpoint — the guard catches them first.</p>
|
||
<p>The client sends the key like this:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ curl -H "X-API-Key: sk-abc123" http://localhost:5042/protected
|
||
</pre></div>
|
||
</div>
|
||
<div class="admonition note">
|
||
<p class="admonition-title">Note</p>
|
||
<p>To read a credential <em>inside</em> a route handler (rather than a guard), use
|
||
a typed parameter marker — <code class="docutils literal notranslate"><span class="pre">api_key:</span> <span class="pre">str</span> <span class="pre">=</span> <span class="pre">Header(None,</span> <span class="pre">alias="X-API-Key")</span></code>
|
||
injects the validated header straight into the handler. See <a class="reference internal" href="tour.html"><span class="doc">Feature Tour</span></a>
|
||
for the full set (<a class="reference internal" href="api.html#responder.Query" title="responder.Query"><code class="xref py py-func docutils literal notranslate"><span class="pre">Query()</span></code></a>, <a class="reference internal" href="api.html#responder.Header" title="responder.Header"><code class="xref py py-func docutils literal notranslate"><span class="pre">Header()</span></code></a>,
|
||
<a class="reference internal" href="api.html#responder.Cookie" title="responder.Cookie"><code class="xref py py-func docutils literal notranslate"><span class="pre">Cookie()</span></code></a>, <a class="reference internal" href="api.html#responder.Path" title="responder.Path"><code class="xref py py-func docutils literal notranslate"><span class="pre">Path()</span></code></a>).</p>
|
||
</div>
|
||
</section>
|
||
<section id="bearer-token-authentication">
|
||
<h2>Bearer Token Authentication<a class="headerlink" href="#bearer-token-authentication" title="Link to this heading">¶</a></h2>
|
||
<p>Bearer tokens are the standard for modern APIs. The client sends a token
|
||
in the <code class="docutils literal notranslate"><span class="pre">Authorization</span></code> header, and the server validates it. The most
|
||
common format is <a class="reference external" href="https://jwt.io/">JWT</a> (JSON Web Tokens).</p>
|
||
<p>Install PyJWT:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ uv pip install pyjwt
|
||
</pre></div>
|
||
</div>
|
||
<p>Create a helper to encode and decode tokens:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">import</span><span class="w"> </span><span class="nn">jwt</span>
|
||
<span class="kn">from</span><span class="w"> </span><span class="nn">datetime</span><span class="w"> </span><span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
|
||
|
||
<span class="n">SECRET</span> <span class="o">=</span> <span class="s2">"your-secret-key"</span> <span class="c1"># load from the environment in production</span>
|
||
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">create_token</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
|
||
<span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
|
||
<span class="s2">"sub"</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span>
|
||
<span class="s2">"exp"</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">24</span><span class="p">),</span>
|
||
<span class="p">}</span>
|
||
<span class="k">return</span> <span class="n">jwt</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">SECRET</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="s2">"HS256"</span><span class="p">)</span>
|
||
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">verify_token</span><span class="p">(</span><span class="n">token</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">dict</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
|
||
<span class="k">try</span><span class="p">:</span>
|
||
<span class="k">return</span> <span class="n">jwt</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">SECRET</span><span class="p">,</span> <span class="n">algorithms</span><span class="o">=</span><span class="p">[</span><span class="s2">"HS256"</span><span class="p">])</span>
|
||
<span class="k">except</span> <span class="n">jwt</span><span class="o">.</span><span class="n">InvalidTokenError</span><span class="p">:</span>
|
||
<span class="k">return</span> <span class="kc">None</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Add a login endpoint that issues tokens, and a before-request hook that
|
||
verifies them:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/login"</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s2">"POST"</span><span class="p">])</span>
|
||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">login</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">req</span><span class="o">.</span><span class="n">media</span><span class="p">()</span>
|
||
<span class="c1"># In a real app, check credentials against a database</span>
|
||
<span class="k">if</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"username"</span><span class="p">)</span> <span class="o">==</span> <span class="s2">"admin"</span> <span class="ow">and</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"password"</span><span class="p">)</span> <span class="o">==</span> <span class="s2">"secret"</span><span class="p">:</span>
|
||
<span class="n">token</span> <span class="o">=</span> <span class="n">create_token</span><span class="p">(</span><span class="n">user_id</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"token"</span><span class="p">:</span> <span class="n">token</span><span class="p">}</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">401</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"error"</span><span class="p">:</span> <span class="s2">"Invalid credentials"</span><span class="p">}</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="n">before_request</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">auth_guard</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="c1"># Skip auth for the login endpoint itself</span>
|
||
<span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">url</span><span class="o">.</span><span class="n">path</span> <span class="o">==</span> <span class="s2">"/login"</span><span class="p">:</span>
|
||
<span class="k">return</span>
|
||
|
||
<span class="n">auth</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"Authorization"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
|
||
<span class="k">if</span> <span class="ow">not</span> <span class="n">auth</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"Bearer "</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">401</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"error"</span><span class="p">:</span> <span class="s2">"Missing bearer token"</span><span class="p">}</span>
|
||
<span class="k">return</span>
|
||
|
||
<span class="n">token</span> <span class="o">=</span> <span class="n">auth</span><span class="p">[</span><span class="mi">7</span><span class="p">:]</span> <span class="c1"># Strip "Bearer "</span>
|
||
<span class="n">payload</span> <span class="o">=</span> <span class="n">verify_token</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>
|
||
<span class="k">if</span> <span class="n">payload</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">401</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"error"</span><span class="p">:</span> <span class="s2">"Invalid or expired token"</span><span class="p">}</span>
|
||
<span class="k">return</span>
|
||
|
||
<span class="c1"># Store the authenticated user on the request state</span>
|
||
<span class="n">req</span><span class="o">.</span><span class="n">state</span><span class="o">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="n">payload</span><span class="p">[</span><span class="s2">"sub"</span><span class="p">]</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Now any route can access the authenticated user:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/me"</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">get_me</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"user_id"</span><span class="p">:</span> <span class="n">req</span><span class="o">.</span><span class="n">state</span><span class="o">.</span><span class="n">user_id</span><span class="p">}</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>The client flow:</p>
|
||
<ol class="arabic simple">
|
||
<li><p><code class="docutils literal notranslate"><span class="pre">POST</span> <span class="pre">/login</span></code> with credentials → receive a token</p></li>
|
||
<li><p>Include <code class="docutils literal notranslate"><span class="pre">Authorization:</span> <span class="pre">Bearer</span> <span class="pre"><token></span></code> on every subsequent request</p></li>
|
||
<li><p>The token expires after 24 hours — the client must log in again</p></li>
|
||
</ol>
|
||
<p>For APIs that use a single auth scheme on selected routes, Responder’s auth
|
||
helpers are more direct than a global guard:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">responder.ext.auth</span><span class="w"> </span><span class="kn">import</span> <span class="n">BearerAuth</span>
|
||
|
||
<span class="n">bearer</span> <span class="o">=</span> <span class="n">BearerAuth</span><span class="p">(</span><span class="n">verify</span><span class="o">=</span><span class="k">lambda</span> <span class="n">token</span><span class="p">:</span> <span class="n">users</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">token</span><span class="p">))</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/me"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="n">bearer</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">get_me</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"user"</span><span class="p">:</span> <span class="n">user</span><span class="p">}</span>
|
||
</pre></div>
|
||
</div>
|
||
<p><code class="docutils literal notranslate"><span class="pre">auth=</span></code> enforces the scheme, sends the correct <code class="docutils literal notranslate"><span class="pre">WWW-Authenticate</span></code> challenge
|
||
on failure, documents the route in OpenAPI when OpenAPI is enabled, and injects
|
||
the verified principal into a <code class="docutils literal notranslate"><span class="pre">user</span></code>, <code class="docutils literal notranslate"><span class="pre">principal</span></code>, or <code class="docutils literal notranslate"><span class="pre">auth</span></code> parameter.</p>
|
||
</section>
|
||
<section id="validating-jwts-with-jwtauth">
|
||
<h2>Validating JWTs with JWTAuth<a class="headerlink" href="#validating-jwts-with-jwtauth" title="Link to this heading">¶</a></h2>
|
||
<p>The hand-rolled guard above works, but
|
||
<code class="xref py py-class docutils literal notranslate"><span class="pre">JWTAuth</span></code> does the whole job — signature
|
||
validation, <code class="docutils literal notranslate"><span class="pre">exp</span></code>/<code class="docutils literal notranslate"><span class="pre">nbf</span></code>/<code class="docutils literal notranslate"><span class="pre">iat</span></code> checks (with configurable clock-skew
|
||
<code class="docutils literal notranslate"><span class="pre">leeway</span></code>), audience and issuer verification, and OpenAPI documentation —
|
||
in one declaration. It needs the optional PyJWT dependency:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ uv pip install 'responder[jwt,orjson]'
|
||
</pre></div>
|
||
</div>
|
||
<p>A real-world setup validates tokens minted by an identity provider for a
|
||
specific API:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">responder.ext.auth</span><span class="w"> </span><span class="kn">import</span> <span class="n">JWTAuth</span>
|
||
|
||
<span class="n">auth</span> <span class="o">=</span> <span class="n">JWTAuth</span><span class="p">(</span>
|
||
<span class="n">secret</span><span class="o">=</span><span class="s2">"your-256-bit-secret"</span><span class="p">,</span> <span class="c1"># HS256 shared secret</span>
|
||
<span class="n">audience</span><span class="o">=</span><span class="s2">"https://api.example.com"</span><span class="p">,</span> <span class="c1"># must match the token's aud</span>
|
||
<span class="n">issuer</span><span class="o">=</span><span class="s2">"https://issuer.example.com"</span><span class="p">,</span> <span class="c1"># must match the token's iss</span>
|
||
<span class="n">leeway</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="c1"># tolerate 30s of clock skew</span>
|
||
<span class="p">)</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"/me"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="n">auth</span><span class="p">)</span>
|
||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">me</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span>
|
||
<span class="c1"># ``user`` is the validated claims dict</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"sub"</span><span class="p">:</span> <span class="n">user</span><span class="p">[</span><span class="s2">"sub"</span><span class="p">],</span> <span class="s2">"scope"</span><span class="p">:</span> <span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"scope"</span><span class="p">)}</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Invalid, expired, or missing tokens reject with <code class="docutils literal notranslate"><span class="pre">401</span></code> and a <code class="docutils literal notranslate"><span class="pre">Bearer</span></code>
|
||
challenge before your handler runs.</p>
|
||
<p>For asymmetric algorithms, pass the issuer’s PEM public key as <code class="docutils literal notranslate"><span class="pre">secret</span></code>
|
||
with <code class="docutils literal notranslate"><span class="pre">algorithms=("RS256",)</span></code> — or skip key management entirely and point
|
||
<code class="docutils literal notranslate"><span class="pre">jwks_url</span></code> at the issuer’s key set. Keys are resolved by the token’s
|
||
<code class="docutils literal notranslate"><span class="pre">kid</span></code> header, cached (<code class="docutils literal notranslate"><span class="pre">jwks_cache_ttl</span></code> seconds), and refreshed
|
||
automatically on key rotation:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">auth</span> <span class="o">=</span> <span class="n">JWTAuth</span><span class="p">(</span>
|
||
<span class="n">jwks_url</span><span class="o">=</span><span class="s2">"https://issuer.example.com/.well-known/jwks.json"</span><span class="p">,</span>
|
||
<span class="n">algorithms</span><span class="o">=</span><span class="p">(</span><span class="s2">"RS256"</span><span class="p">,),</span>
|
||
<span class="n">audience</span><span class="o">=</span><span class="s2">"https://api.example.com"</span><span class="p">,</span>
|
||
<span class="n">issuer</span><span class="o">=</span><span class="s2">"https://issuer.example.com"</span><span class="p">,</span>
|
||
<span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>(JWKS and <code class="docutils literal notranslate"><span class="pre">RS*</span></code>/<code class="docutils literal notranslate"><span class="pre">ES*</span></code> algorithms additionally require the
|
||
<code class="docutils literal notranslate"><span class="pre">cryptography</span></code> package.)</p>
|
||
<p>Because the claims dict is the principal, the standard OAuth2 <code class="docutils literal notranslate"><span class="pre">scope</span></code>
|
||
claim (or a <code class="docutils literal notranslate"><span class="pre">scopes</span></code>/<code class="docutils literal notranslate"><span class="pre">roles</span></code> claim) feeds directly into scope checks —
|
||
<code class="docutils literal notranslate"><span class="pre">requires</span></code> rejects with <code class="docutils literal notranslate"><span class="pre">403</span></code> and an <code class="docutils literal notranslate"><span class="pre">insufficient_scope</span></code> challenge
|
||
when a scope is missing:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"/admin"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="n">auth</span><span class="o">.</span><span class="n">requires</span><span class="p">(</span><span class="s2">"admin"</span><span class="p">))</span>
|
||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">admin_dashboard</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span> <span class="o">...</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>To map claims onto your own user object, pass <code class="docutils literal notranslate"><span class="pre">verify=</span></code> — it receives the
|
||
validated claims and returns the principal to inject (or a falsy value to
|
||
reject):</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">auth</span> <span class="o">=</span> <span class="n">JWTAuth</span><span class="p">(</span><span class="n">secret</span><span class="o">=</span><span class="n">SECRET</span><span class="p">,</span> <span class="n">verify</span><span class="o">=</span><span class="k">lambda</span> <span class="n">claims</span><span class="p">:</span> <span class="n">users</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">claims</span><span class="p">[</span><span class="s2">"sub"</span><span class="p">]))</span>
|
||
</pre></div>
|
||
</div>
|
||
</section>
|
||
<section id="oauth2-and-swagger-ui-s-authorize-button">
|
||
<h2>OAuth2 and Swagger UI’s Authorize Button<a class="headerlink" href="#oauth2-and-swagger-ui-s-authorize-button" title="Link to this heading">¶</a></h2>
|
||
<p>When your tokens come from an OAuth2 authorization server, document the
|
||
flow itself with <code class="xref py py-class docutils literal notranslate"><span class="pre">OAuth2Auth</span></code> — the OpenAPI
|
||
schema then emits a proper <code class="docutils literal notranslate"><span class="pre">type:</span> <span class="pre">oauth2</span></code> security scheme with flows and
|
||
scopes, and Swagger UI’s <em>Authorize</em> button lets consumers log in against
|
||
your issuer right from the docs page:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">responder.ext.auth</span><span class="w"> </span><span class="kn">import</span> <span class="n">JWTAuth</span><span class="p">,</span> <span class="n">OAuth2Auth</span>
|
||
|
||
<span class="n">oauth2</span> <span class="o">=</span> <span class="n">OAuth2Auth</span><span class="o">.</span><span class="n">authorization_code</span><span class="p">(</span>
|
||
<span class="s2">"https://issuer.example.com/authorize"</span><span class="p">,</span>
|
||
<span class="s2">"https://issuer.example.com/oauth/token"</span><span class="p">,</span>
|
||
<span class="n">scopes</span><span class="o">=</span><span class="p">{</span><span class="s2">"read"</span><span class="p">:</span> <span class="s2">"Read your data"</span><span class="p">,</span> <span class="s2">"write"</span><span class="p">:</span> <span class="s2">"Modify your data"</span><span class="p">},</span>
|
||
<span class="n">jwt</span><span class="o">=</span><span class="n">JWTAuth</span><span class="p">(</span>
|
||
<span class="n">jwks_url</span><span class="o">=</span><span class="s2">"https://issuer.example.com/.well-known/jwks.json"</span><span class="p">,</span>
|
||
<span class="n">algorithms</span><span class="o">=</span><span class="p">(</span><span class="s2">"RS256"</span><span class="p">,),</span>
|
||
<span class="n">audience</span><span class="o">=</span><span class="s2">"https://api.example.com"</span><span class="p">,</span>
|
||
<span class="p">),</span>
|
||
<span class="p">)</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"/items"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="n">oauth2</span><span class="o">.</span><span class="n">requires</span><span class="p">(</span><span class="s2">"read"</span><span class="p">))</span>
|
||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">list_items</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="n">load_items</span><span class="p">(</span><span class="n">owner</span><span class="o">=</span><span class="n">user</span><span class="p">[</span><span class="s2">"sub"</span><span class="p">])</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>At runtime, <code class="docutils literal notranslate"><span class="pre">OAuth2Auth</span></code> is a resource server, not an authorization
|
||
server: it extracts the <code class="docutils literal notranslate"><span class="pre">Authorization:</span> <span class="pre">Bearer</span></code> token and validates it,
|
||
either locally through the <code class="docutils literal notranslate"><span class="pre">jwt=</span></code> <code class="xref py py-class docutils literal notranslate"><span class="pre">JWTAuth</span></code>
|
||
(as above) or through a <code class="docutils literal notranslate"><span class="pre">verify=</span></code> introspection callable for opaque
|
||
tokens:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">introspect</span><span class="p">(</span><span class="n">token</span><span class="p">):</span>
|
||
<span class="k">async</span> <span class="k">with</span> <span class="n">httpx</span><span class="o">.</span><span class="n">AsyncClient</span><span class="p">()</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
|
||
<span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">INTROSPECTION_URL</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="s2">"token"</span><span class="p">:</span> <span class="n">token</span><span class="p">})</span>
|
||
<span class="n">data</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
|
||
<span class="k">return</span> <span class="n">data</span> <span class="k">if</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"active"</span><span class="p">)</span> <span class="k">else</span> <span class="kc">None</span>
|
||
|
||
<span class="n">oauth2</span> <span class="o">=</span> <span class="n">OAuth2Auth</span><span class="o">.</span><span class="n">client_credentials</span><span class="p">(</span>
|
||
<span class="s2">"https://issuer.example.com/oauth/token"</span><span class="p">,</span>
|
||
<span class="n">scopes</span><span class="o">=</span><span class="p">{</span><span class="s2">"svc"</span><span class="p">:</span> <span class="s2">"Service-to-service access"</span><span class="p">},</span>
|
||
<span class="n">verify</span><span class="o">=</span><span class="n">introspect</span><span class="p">,</span>
|
||
<span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Besides <code class="docutils literal notranslate"><span class="pre">authorization_code</span></code> (interactive user login) and
|
||
<code class="docutils literal notranslate"><span class="pre">client_credentials</span></code> (machine-to-machine), a <code class="docutils literal notranslate"><span class="pre">password</span></code> constructor
|
||
covers the resource-owner-password flow, and multiple flows can be combined
|
||
by passing flow objects directly:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">responder.ext.auth</span><span class="w"> </span><span class="kn">import</span> <span class="p">(</span>
|
||
<span class="n">OAuth2Auth</span><span class="p">,</span>
|
||
<span class="n">OAuth2AuthorizationCodeFlow</span><span class="p">,</span>
|
||
<span class="n">OAuth2ClientCredentialsFlow</span><span class="p">,</span>
|
||
<span class="p">)</span>
|
||
|
||
<span class="n">oauth2</span> <span class="o">=</span> <span class="n">OAuth2Auth</span><span class="p">(</span>
|
||
<span class="p">[</span>
|
||
<span class="n">OAuth2AuthorizationCodeFlow</span><span class="p">(</span><span class="n">AUTHORIZE_URL</span><span class="p">,</span> <span class="n">TOKEN_URL</span><span class="p">,</span> <span class="n">scopes</span><span class="o">=</span><span class="n">SCOPES</span><span class="p">),</span>
|
||
<span class="n">OAuth2ClientCredentialsFlow</span><span class="p">(</span><span class="n">TOKEN_URL</span><span class="p">,</span> <span class="n">scopes</span><span class="o">=</span><span class="n">SCOPES</span><span class="p">),</span>
|
||
<span class="p">],</span>
|
||
<span class="n">jwt</span><span class="o">=</span><span class="n">JWTAuth</span><span class="p">(</span><span class="n">jwks_url</span><span class="o">=</span><span class="n">JWKS_URL</span><span class="p">,</span> <span class="n">algorithms</span><span class="o">=</span><span class="p">(</span><span class="s2">"RS256"</span><span class="p">,)),</span>
|
||
<span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Like every auth helper, both classes compose with routers — declare
|
||
<code class="docutils literal notranslate"><span class="pre">Router(prefix="/v1",</span> <span class="pre">auth=oauth2)</span></code> (or
|
||
<code class="docutils literal notranslate"><span class="pre">api.include_router(router,</span> <span class="pre">auth=oauth2)</span></code>) to protect a whole group of
|
||
routes at once. See <a class="reference internal" href="routers.html"><span class="doc">Composing Apps with Routers</span></a>.</p>
|
||
</section>
|
||
<section id="skipping-auth-for-public-routes">
|
||
<h2>Skipping Auth for Public Routes<a class="headerlink" href="#skipping-auth-for-public-routes" title="Link to this heading">¶</a></h2>
|
||
<p>For APIs where most routes require the same auth scheme, put it on the app and
|
||
mark public routes explicitly:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">bearer</span> <span class="o">=</span> <span class="n">BearerAuth</span><span class="p">(</span><span class="n">verify</span><span class="o">=</span><span class="k">lambda</span> <span class="n">token</span><span class="p">:</span> <span class="n">users</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">token</span><span class="p">))</span>
|
||
<span class="n">api</span> <span class="o">=</span> <span class="n">responder</span><span class="o">.</span><span class="n">API</span><span class="p">(</span><span class="n">auth</span><span class="o">=</span><span class="n">bearer</span><span class="p">)</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">"/login"</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">login</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"token"</span><span class="p">:</span> <span class="n">issue_token</span><span class="p">()}</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"/me"</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">me</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">,</span> <span class="o">*</span><span class="p">,</span> <span class="n">user</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"user"</span><span class="p">:</span> <span class="n">user</span><span class="p">}</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Routes inherit <code class="docutils literal notranslate"><span class="pre">API(auth=...)</span></code> by default. Passing <code class="docutils literal notranslate"><span class="pre">auth=None</span></code> on a route
|
||
makes that route public and removes the inherited OpenAPI security requirement.</p>
|
||
</section>
|
||
<section id="custom-exception-for-auth-errors">
|
||
<h2>Custom Exception for Auth Errors<a class="headerlink" href="#custom-exception-for-auth-errors" title="Link to this heading">¶</a></h2>
|
||
<p>For a one-off rejection, reach for <a class="reference internal" href="api.html#responder.abort" title="responder.abort"><code class="xref py py-func docutils literal notranslate"><span class="pre">abort()</span></code></a> — it raises a
|
||
rendered HTTP error from anywhere in the request path (handler, hook, or
|
||
dependency) without importing Starlette:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">responder</span><span class="w"> </span><span class="kn">import</span> <span class="n">abort</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="n">before_request</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">auth_guard</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">url</span><span class="o">.</span><span class="n">path</span> <span class="ow">in</span> <span class="n">PUBLIC_PATHS</span><span class="p">:</span>
|
||
<span class="k">return</span>
|
||
<span class="k">if</span> <span class="s2">"Authorization"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">req</span><span class="o">.</span><span class="n">headers</span><span class="p">:</span>
|
||
<span class="n">abort</span><span class="p">(</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="s2">"Missing authorization header"</span><span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
<p><code class="docutils literal notranslate"><span class="pre">abort</span></code> renders as JSON for <code class="docutils literal notranslate"><span class="pre">Accept:</span> <span class="pre">application/json</span></code> clients and plain
|
||
text otherwise.</p>
|
||
<p>When you want a <em>structured</em> error payload, define a custom exception and
|
||
register a handler instead:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">class</span><span class="w"> </span><span class="nc">AuthError</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="o">=</span><span class="s2">"Unauthorized"</span><span class="p">,</span> <span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">):</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">message</span> <span class="o">=</span> <span class="n">message</span>
|
||
<span class="bp">self</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="n">status_code</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">exception_handler</span><span class="p">(</span><span class="n">AuthError</span><span class="p">)</span>
|
||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">handle_auth_error</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">,</span> <span class="n">exc</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="n">exc</span><span class="o">.</span><span class="n">status_code</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"error"</span><span class="p">:</span> <span class="n">exc</span><span class="o">.</span><span class="n">message</span><span class="p">}</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Now your auth guard can simply raise:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="n">before_request</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">auth_guard</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">url</span><span class="o">.</span><span class="n">path</span> <span class="ow">in</span> <span class="n">PUBLIC_PATHS</span><span class="p">:</span>
|
||
<span class="k">return</span>
|
||
<span class="k">if</span> <span class="s2">"Authorization"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">req</span><span class="o">.</span><span class="n">headers</span><span class="p">:</span>
|
||
<span class="k">raise</span> <span class="n">AuthError</span><span class="p">(</span><span class="s2">"Missing authorization header"</span><span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>The decorator delegates to
|
||
<code class="docutils literal notranslate"><span class="pre">api.add_exception_handler(AuthError,</span> <span class="pre">handle_auth_error)</span></code> — call that form
|
||
directly when you wire handlers from a setup function rather than at import
|
||
time.</p>
|
||
</section>
|
||
<section id="using-sessions-for-web-apps">
|
||
<h2>Using Sessions for Web Apps<a class="headerlink" href="#using-sessions-for-web-apps" title="Link to this heading">¶</a></h2>
|
||
<p>For traditional web applications (with HTML pages and forms), cookie-based
|
||
sessions are simpler than tokens. The browser handles cookies automatically
|
||
— no client-side token management needed:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">responder.ext.sessions</span><span class="w"> </span><span class="kn">import</span> <span class="n">regenerate_session</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/login"</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s2">"POST"</span><span class="p">])</span>
|
||
<span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">login</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">req</span><span class="o">.</span><span class="n">media</span><span class="p">(</span><span class="s2">"form"</span><span class="p">)</span>
|
||
<span class="k">if</span> <span class="n">data</span><span class="p">[</span><span class="s2">"username"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"admin"</span> <span class="ow">and</span> <span class="n">data</span><span class="p">[</span><span class="s2">"password"</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"secret"</span><span class="p">:</span>
|
||
<span class="n">regenerate_session</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="c1"># rotate the id on login</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">session</span><span class="p">[</span><span class="s2">"user"</span><span class="p">]</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="s2">"username"</span><span class="p">]</span>
|
||
<span class="c1"># Honor ?next=, but never bounce off-site (open-redirect guard).</span>
|
||
<span class="n">target</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">params</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"next"</span><span class="p">,</span> <span class="s2">"/dashboard"</span><span class="p">)</span>
|
||
<span class="n">api</span><span class="o">.</span><span class="n">redirect</span><span class="p">(</span><span class="n">resp</span><span class="p">,</span> <span class="n">location</span><span class="o">=</span><span class="n">target</span><span class="p">,</span> <span class="n">allow_external</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
|
||
<span class="k">else</span><span class="p">:</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">401</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">html</span> <span class="o">=</span> <span class="s2">"<p>Invalid credentials</p>"</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/dashboard"</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">dashboard</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">user</span> <span class="o">=</span> <span class="n">req</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"user"</span><span class="p">)</span>
|
||
<span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
|
||
<span class="n">api</span><span class="o">.</span><span class="n">redirect</span><span class="p">(</span><span class="n">resp</span><span class="p">,</span> <span class="n">location</span><span class="o">=</span><span class="s2">"/login"</span><span class="p">)</span>
|
||
<span class="k">return</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">html</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"<h1>Welcome, </span><span class="si">{</span><span class="n">user</span><span class="si">}</span><span class="s2">!</h1>"</span>
|
||
|
||
<span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/logout"</span><span class="p">)</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">logout</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
|
||
<span class="n">api</span><span class="o">.</span><span class="n">redirect</span><span class="p">(</span><span class="n">resp</span><span class="p">,</span> <span class="n">location</span><span class="o">=</span><span class="s2">"/login"</span><span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Two security details in that login handler are worth calling out:</p>
|
||
<ul class="simple">
|
||
<li><p><strong>Rotate the session id on login.</strong> <code class="docutils literal notranslate"><span class="pre">regenerate_session(req)</span></code> issues a
|
||
fresh id after the privilege change, defeating session fixation. It takes
|
||
effect with a server-side <code class="docutils literal notranslate"><span class="pre">session_backend</span></code> (see <a class="reference internal" href="guide-config.html"><span class="doc">Configuration</span></a>);
|
||
for the default signed-cookie sessions the cookie <em>is</em> the session, so the
|
||
call is a harmless no-op.</p></li>
|
||
<li><p><strong>Guard user-controlled redirects.</strong> <code class="docutils literal notranslate"><span class="pre">api.redirect</span></code> allows external URLs
|
||
by default; pass <code class="docutils literal notranslate"><span class="pre">allow_external=False</span></code> for any target derived from user
|
||
input (like <code class="docutils literal notranslate"><span class="pre">?next=</span></code>) so an attacker can’t craft
|
||
<code class="docutils literal notranslate"><span class="pre">?next=https://evil.example</span></code> to bounce a freshly authenticated user
|
||
off-site. An external target then returns <code class="docutils literal notranslate"><span class="pre">400</span></code>.</p></li>
|
||
</ul>
|
||
<p>Sessions are secure by default — just set a stable secret key in
|
||
production:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">api</span> <span class="o">=</span> <span class="n">responder</span><span class="o">.</span><span class="n">API</span><span class="p">(</span><span class="n">secret_key</span><span class="o">=</span><span class="s2">"<a long, random string>"</span><span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Generate one with:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span>$ python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||
</pre></div>
|
||
</div>
|
||
<p>Better still, supply it out of band via the <code class="docutils literal notranslate"><span class="pre">RESPONDER_SECRET_KEY</span></code>
|
||
environment variable so it never lands in source control.</p>
|
||
<div class="admonition note">
|
||
<p class="admonition-title">Note</p>
|
||
<p>The old public default is gone: <code class="docutils literal notranslate"><span class="pre">secret_key="NOTASECRET"</span></code> now raises
|
||
<a class="reference internal" href="api.html#responder.ext.sessions.SessionConfigError" title="responder.ext.sessions.SessionConfigError"><code class="xref py py-class docutils literal notranslate"><span class="pre">SessionConfigError</span></code></a>. With the default
|
||
<code class="docutils literal notranslate"><span class="pre">sessions="auto"</span></code>, omitting a key mints a random per-process key and
|
||
logs a loud warning — fine for a quick local run, but it breaks across
|
||
restarts and multiple workers (everyone gets logged out). Set a real key
|
||
for anything you deploy. The cookie is also <code class="docutils literal notranslate"><span class="pre">Secure</span></code> in production by
|
||
default; pass <code class="docutils literal notranslate"><span class="pre">session_https_only=False</span></code> only if you genuinely serve
|
||
plain HTTP. See <a class="reference internal" href="guide-config.html"><span class="doc">Configuration</span></a> for the full set of session knobs
|
||
(<code class="docutils literal notranslate"><span class="pre">session_cookie</span></code>, <code class="docutils literal notranslate"><span class="pre">session_https_only</span></code>, <code class="docutils literal notranslate"><span class="pre">session_same_site</span></code>,
|
||
<code class="docutils literal notranslate"><span class="pre">session_max_age</span></code>, <code class="docutils literal notranslate"><span class="pre">session_backend</span></code>).</p>
|
||
</div>
|
||
<p>Session data is signed, not encrypted — clients can read it but can’t forge
|
||
it. Never store secrets like passwords in the session.</p>
|
||
</section>
|
||
<section id="role-based-access-control">
|
||
<h2>Role-Based Access Control<a class="headerlink" href="#role-based-access-control" title="Link to this heading">¶</a></h2>
|
||
<p>For APIs where different users have different permissions, embed the
|
||
role in the token and check it in route-specific guards:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">def</span><span class="w"> </span><span class="nf">create_token</span><span class="p">(</span><span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">role</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">"user"</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
|
||
<span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
|
||
<span class="s2">"sub"</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span>
|
||
<span class="s2">"role"</span><span class="p">:</span> <span class="n">role</span><span class="p">,</span>
|
||
<span class="s2">"exp"</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">24</span><span class="p">),</span>
|
||
<span class="p">}</span>
|
||
<span class="k">return</span> <span class="n">jwt</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">SECRET</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="s2">"HS256"</span><span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Create a helper that checks for a specific role:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="kn">from</span><span class="w"> </span><span class="nn">responder.types</span><span class="w"> </span><span class="kn">import</span> <span class="n">Hook</span>
|
||
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">require_role</span><span class="p">(</span><span class="o">*</span><span class="n">roles</span><span class="p">)</span> <span class="o">-></span> <span class="n">Hook</span><span class="p">:</span>
|
||
<span class="w"> </span><span class="sd">"""Before-request hook factory that restricts by role."""</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">check</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">user_role</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">state</span><span class="p">,</span> <span class="s2">"role"</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
|
||
<span class="k">if</span> <span class="n">user_role</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">roles</span><span class="p">:</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">status_code</span> <span class="o">=</span> <span class="mi">403</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"error"</span><span class="p">:</span> <span class="s2">"Insufficient permissions"</span><span class="p">}</span>
|
||
<span class="k">return</span> <span class="n">check</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>Use it on specific routes:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="nd">@api</span><span class="o">.</span><span class="n">route</span><span class="p">(</span><span class="s2">"/admin/users"</span><span class="p">,</span> <span class="n">before</span><span class="o">=</span><span class="n">require_role</span><span class="p">(</span><span class="s2">"admin"</span><span class="p">))</span>
|
||
<span class="k">def</span><span class="w"> </span><span class="nf">list_all_users</span><span class="p">(</span><span class="n">req</span><span class="p">,</span> <span class="n">resp</span><span class="p">):</span>
|
||
<span class="n">resp</span><span class="o">.</span><span class="n">media</span> <span class="o">=</span> <span class="p">{</span><span class="s2">"users"</span><span class="p">:</span> <span class="p">[</span><span class="o">...</span><span class="p">]}</span>
|
||
</pre></div>
|
||
</div>
|
||
<p>And store the role during token verification:</p>
|
||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># In your auth_guard:</span>
|
||
<span class="n">req</span><span class="o">.</span><span class="n">state</span><span class="o">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="n">payload</span><span class="p">[</span><span class="s2">"sub"</span><span class="p">]</span>
|
||
<span class="n">req</span><span class="o">.</span><span class="n">state</span><span class="o">.</span><span class="n">role</span> <span class="o">=</span> <span class="n">payload</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"role"</span><span class="p">,</span> <span class="s2">"user"</span><span class="p">)</span>
|
||
</pre></div>
|
||
</div>
|
||
</section>
|
||
<section id="choosing-an-auth-strategy">
|
||
<h2>Choosing an Auth Strategy<a class="headerlink" href="#choosing-an-auth-strategy" title="Link to this heading">¶</a></h2>
|
||
<ul class="simple">
|
||
<li><p><strong>API keys</strong> — simplest. Good for server-to-server, CLI tools, and
|
||
internal services. No expiration unless you build it.</p></li>
|
||
<li><p><strong>JWT tokens</strong> — standard for SPAs and mobile apps. Stateless, so
|
||
they scale well. Downside: you can’t revoke them without a blocklist.</p></li>
|
||
<li><p><strong>Sessions</strong> — best for traditional web apps with HTML forms. The
|
||
browser manages cookies automatically. Stateful — the server controls
|
||
the session lifecycle.</p></li>
|
||
</ul>
|
||
<p>Start with API keys for internal tools, JWT for public APIs, and
|
||
sessions for web apps with login pages.</p>
|
||
</section>
|
||
</section>
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
<div class="sphinxsidebar" role="navigation" aria-label="Main">
|
||
<div class="sphinxsidebarwrapper"><p class="logo">
|
||
<a href="index.html">
|
||
<img class="logo" src="_static/responder.png" />
|
||
</a>
|
||
</p>
|
||
<p>
|
||
<strong>Responder</strong> — a familiar HTTP service framework for Python.
|
||
<br />
|
||
<small>v9.2.0</small>
|
||
</p>
|
||
<h3>Useful Links</h3>
|
||
<ul>
|
||
<li><a href="https://github.com/kennethreitz/responder">Responder @ GitHub</a></li>
|
||
<li><a href="https://pypi.org/project/responder/">Responder @ PyPI</a></li>
|
||
<li><a href="https://github.com/kennethreitz/responder/issues">Issue Tracker</a></li>
|
||
</ul><h3>Navigation</h3>
|
||
<p class="caption" role="heading"><span class="caption-text">User Guide</span></p>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="quickstart.html">Quick Start</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="tour.html">Feature Tour</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Examples</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="routers.html">Composing Apps with Routers</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="guide-config.html">Configuration</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="clientgen.html">Generated Clients</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="deployment.html">Deployment</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="testing.html">Testing</a></li>
|
||
</ul>
|
||
<p class="caption" role="heading"><span class="caption-text">Tutorials</span></p>
|
||
<ul class="current">
|
||
<li class="toctree-l1"><a class="reference internal" href="tutorial-rest.html">Building a REST API</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="tutorial-sqlalchemy.html">Using SQLAlchemy</a></li>
|
||
<li class="toctree-l1 current"><a class="current reference internal" href="#">Authentication</a><ul>
|
||
<li class="toctree-l2"><a class="reference internal" href="#api-key-authentication">API Key Authentication</a></li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#bearer-token-authentication">Bearer Token Authentication</a></li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#validating-jwts-with-jwtauth">Validating JWTs with JWTAuth</a></li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#oauth2-and-swagger-ui-s-authorize-button">OAuth2 and Swagger UI’s Authorize Button</a></li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#skipping-auth-for-public-routes">Skipping Auth for Public Routes</a></li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#custom-exception-for-auth-errors">Custom Exception for Auth Errors</a></li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#using-sessions-for-web-apps">Using Sessions for Web Apps</a></li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#role-based-access-control">Role-Based Access Control</a></li>
|
||
<li class="toctree-l2"><a class="reference internal" href="#choosing-an-auth-strategy">Choosing an Auth Strategy</a></li>
|
||
</ul>
|
||
</li>
|
||
<li class="toctree-l1"><a class="reference internal" href="tutorial-websockets.html">WebSocket Tutorial</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="tutorial-middleware.html">Writing Middleware</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="tutorial-flask.html">Migrating from Flask</a></li>
|
||
</ul>
|
||
<p class="caption" role="heading"><span class="caption-text">Reference</span></p>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="api.html">API Reference</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="cli.html">Command Line Interface</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="runtime-contracts.html">Runtime Contracts</a></li>
|
||
</ul>
|
||
<p class="caption" role="heading"><span class="caption-text">Project</span></p>
|
||
<ul>
|
||
<li class="toctree-l1"><a class="reference internal" href="changes.html">Changelog</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="migration-v9.html">Migrating to v9</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="migration-v8.html">Migrating to v8</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="migration-v7.html">Migrating to v7</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="migration-v6.html">Migrating to v6</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="migration-v5.html">Migrating to v5</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="sandbox.html">Sandbox</a></li>
|
||
<li class="toctree-l1"><a class="reference internal" href="backlog.html">Backlog</a></li>
|
||
</ul>
|
||
|
||
<div class="relations">
|
||
<h3>Related Topics</h3>
|
||
<ul>
|
||
<li><a href="index.html">Documentation overview</a><ul>
|
||
<li>Previous: <a href="tutorial-sqlalchemy.html" title="previous chapter">Using SQLAlchemy</a></li>
|
||
<li>Next: <a href="tutorial-websockets.html" title="next chapter">WebSocket Tutorial</a></li>
|
||
</ul></li>
|
||
</ul>
|
||
</div>
|
||
<search id="searchbox" style="display: none" role="search">
|
||
<h3 id="searchlabel">Quick search</h3>
|
||
<div class="searchformwrapper">
|
||
<form class="search" action="search.html" method="get">
|
||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||
<input type="submit" value="Go" />
|
||
</form>
|
||
</div>
|
||
</search>
|
||
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||
</div>
|
||
</div>
|
||
<div class="clearer"></div>
|
||
</div>
|
||
<div class="footer">
|
||
©2018-2026, Kenneth Reitz.
|
||
|
||
|
|
||
<a href="_sources/tutorial-auth.rst.txt"
|
||
rel="nofollow">Page source</a>
|
||
</div>
|
||
|
||
|
||
<a href="https://github.com/kennethreitz/responder" class="github">
|
||
<img src="_static/forkme_right_darkblue.png" alt="Fork me on GitHub" class="github"/>
|
||
</a>
|
||
|
||
|
||
|
||
</body>
|
||
</html> |