<?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>Forward-Auth – Serverküche</title><link>https://serverkueche.de/en/tags/forward-auth/</link><description>Forward-Auth – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Thu, 08 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/forward-auth/index.xml" rel="self" type="application/rss+xml"/><item><title>Authentik Part 2: Connecting Apps – Forward Auth &amp; OIDC</title><link>https://serverkueche.de/en/tutorials/authentik-connect-apps/</link><pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/authentik-connect-apps/</guid><description>Attach services to Authentik: forward auth with Traefik for apps without a login, and OpenID Connect for everything with its own SSO support – all verified.</description><content:encoded><![CDATA[<p>Authentik is running – now comes the part that justifies the effort: the services behind it. There are
exactly two routes, and the choice depends solely on whether the application understands SSO itself.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>We attach two services, each the appropriate way:</p>
<ul>
<li><strong>Forward auth</strong> for an application <strong>without</strong> its own login. Traefik asks Authentik before every
request whether the visitor is signed in. If not, they land on the Authentik login page; if they are,
the application receives their identity <strong>as HTTP headers</strong>.</li>
<li><strong>OpenID Connect (OIDC)</strong> for applications <strong>with</strong> SSO support – Grafana, Nextcloud, Forgejo, Immich
and many others. There you enter client ID, secret and the endpoints, and the app does the rest.</li>
</ul>
<p>Everything below was carried out with <strong>Authentik 2026.8</strong> behind Traefik v3.7 on Debian 13: at the
end the test service shows the forwarded identity headers, and the OIDC discovery document returns real
endpoints.</p>
<h3 id="which-route-for-which-service">Which route for which service?</h3>
<p>The decision is almost always clear-cut:</p>
<table>
	<thead>
			<tr>
					<th>Service</th>
					<th>Supports OIDC?</th>
					<th>Route</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Nextcloud, Grafana, Forgejo, Immich, Paperless</td>
					<td>yes</td>
					<td><strong>OIDC</strong> – real accounts, roles from groups</td>
			</tr>
			<tr>
					<td>Uptime Kuma, Dockge, Netdata, a static dashboard</td>
					<td>no</td>
					<td><strong>forward auth</strong></td>
			</tr>
			<tr>
					<td>Metrics endpoints, <code>/actuator</code>, internal APIs</td>
					<td>irrelevant</td>
					<td>forward auth <strong>or</strong>, simpler: not public (VPN)</td>
			</tr>
			<tr>
					<td>Services with a mobile app (Immich, Nextcloud)</td>
					<td>yes</td>
					<td><strong>OIDC only</strong> – forward auth breaks the apps (see below)</td>
			</tr>
	</tbody>
</table>
<p>In short: <strong>OIDC where possible, forward auth where necessary.</strong> OIDC gives the application a real user
account including roles; forward auth only puts a door in front of it and passes a name through in a
header.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <strong>running Authentik</strong> from <a href="/en/tutorials/authentik-single-sign-on/">part 1</a>, reachable at
<code>https://YOUR_DOMAIN</code></li>
<li>A <strong>Traefik v3</strong> with a <code>proxy</code> network (<a href="/en/tutorials/traefik-reverse-proxy/">guide</a>) – we work a
lot with middlewares here, the basics are in
<a href="/en/tutorials/traefik-middlewares-hardening/">hardening Traefik</a></li>
<li>A service to practise on. We use <code>traefik/whoami</code> because it echoes every header it receives – which
makes it immediately obvious whether the integration really works</li>
</ul>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>One domain or two?
  </p>
  <div class="prose-kitchen text-sm">We show the setup <strong>on one domain</strong>: Authentik at <code>https://YOUR_DOMAIN/</code>, the protected service at
<code>https://YOUR_DOMAIN/whoami</code>. That is the frugal variant and exactly how it was tested. More common is
the split (<code>auth.YOUR_DOMAIN</code> for Authentik, <code>app.YOUR_DOMAIN</code> for the service) – the steps are
identical, only <code>external_host</code> and the Traefik rules then carry the other hostname.</div>
</div>
<h2 id="step-by-step">Step by step</h2>
<h3 id="1-access-for-automation-an-api-token">1. Access for automation: an API token</h3>
<p>Providers and applications can be created entirely through the API – handy, because it makes your work
reproducible instead of click-based. For that, Authentik needs a token.</p>
<p>Because you went through the setup wizard in part 1, <code>akadmin</code> already exists. So you create the
token <strong>in the interface</strong>: <strong>Directory → Tokens and App passwords → Create token</strong>. Use a
descriptive identifier (<code>api-setup</code>), set the intent to <strong>API</strong> and the user to <code>akadmin</code>. After
saving, copy the key via the copy icon – it is shown only once.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>On a fresh installation it can be automatic
  </p>
  <div class="prose-kitchen text-sm"><p>If you set Authentik up from scratch, you can have the account and token created on the very
<strong>first</strong> start – no wizard needed. The variables belong on the <strong><code>worker</code></strong> service (not on
<code>server</code>) and are <strong>only read on the first startup</strong>; on an existing installation they have no
effect. Instead of the plaintext password, Authentik now recommends a hash. <code>hash_password</code> does <strong>not</strong>
take the password as an argument – <code>hash_password YOUR_PASSWORD</code> aborts with
<code>unrecognized arguments</code> – but via a prompt or standard input:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose run --rm server hash_password</span></span></code></pre></div>
</div>
<p>At the end you get a line starting with <code>pbkdf2_sha256$</code>. It goes into the <code>.env</code> as
<code>AK_BOOTSTRAP_PASSWORD_HASH</code>, the token as <code>AK_BOOTSTRAP_TOKEN</code> (for example from
<code>openssl rand -hex 32</code>). In <code>compose.yaml</code> the <code>worker</code> then looks like this:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">worker</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_BOOTSTRAP_PASSWORD_HASH</span><span class="p">:</span><span class="w"> </span><span class="l">${AK_BOOTSTRAP_PASSWORD_HASH}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_BOOTSTRAP_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="l">${AK_BOOTSTRAP_TOKEN}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_BOOTSTRAP_EMAIL</span><span class="p">:</span><span class="w"> </span><span class="l">you@YOUR_DOMAIN</span></span></span></code></pre></div>
</div>
<p>The hash contains <code>$</code> characters: wrap it in <strong>single</strong> quotes in the <code>.env</code>, and write each <code>$</code>
as <code>$$</code> when putting it directly into <code>compose.yaml</code>.</p>
</div>
</div>
<p>Verify the token – no matter which route produced it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -H <span class="s2">&#34;Authorization: Bearer YOUR_LONG_RANDOM_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://YOUR_DOMAIN/api/v3/core/users/me/</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;user&#34;:{&#34;username&#34;:&#34;akadmin&#34;, … &#34;is_superuser&#34;:true, …}}</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm">The bootstrap token is a <strong>full admin access without a second factor</strong>. It belongs in the <code>.env</code> with
<code>chmod 600</code>, not in a script in your repository – and if you only need it for setup, delete it
afterwards under <strong>Directory → Tokens</strong>.</div>
</div>
<h3 id="2-route-a-forward-auth-for-services-without-a-login">2. Route A: forward auth for services without a login</h3>
<p>The proxy provider is Authentik&rsquo;s answer to „this app has no user management at all&quot;. It comes in
several modes; we use <strong><code>forward_single</code></strong>: one application, one provider.</p>
<p>First the IDs of the two default flows (known from part 1):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">API</span><span class="o">=</span>https://YOUR_DOMAIN/api/v3
</span></span><span class="line"><span class="cl"><span class="nv">TOK</span><span class="o">=</span><span class="s2">&#34;Authorization: Bearer YOUR_LONG_RANDOM_TOKEN&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">AUTHZ</span><span class="o">=</span><span class="k">$(</span>curl -s -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/flows/instances/?slug=default-provider-authorization-implicit-consent&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -c <span class="s2">&#34;import json,sys; print(json.load(sys.stdin)[&#39;results&#39;][0][&#39;pk&#39;])&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">INVAL</span><span class="o">=</span><span class="k">$(</span>curl -s -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/flows/instances/?slug=default-provider-invalidation-flow&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -c <span class="s2">&#34;import json,sys; print(json.load(sys.stdin)[&#39;results&#39;][0][&#39;pk&#39;])&#34;</span><span class="k">)</span></span></span></code></pre></div>
</div>
<p>Then the provider:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/providers/proxy/&#34;</span> -d <span class="s2">&#34;{
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;name\&#34;: \&#34;whoami-forwardauth\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;authorization_flow\&#34;: \&#34;</span><span class="nv">$AUTHZ</span><span class="s2">\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;invalidation_flow\&#34;: \&#34;</span><span class="nv">$INVAL</span><span class="s2">\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;mode\&#34;: \&#34;forward_single\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;external_host\&#34;: \&#34;https://YOUR_DOMAIN\&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">}&#34;</span></span></span></code></pre></div>
</div>
<p>And the <strong>application</strong> that uses it – it also creates the tile in the user library:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/core/applications/&#34;</span> -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;name&#34;: &#34;whoami (protected)&#34;, &#34;slug&#34;: &#34;whoami&#34;, &#34;provider&#34;: 1
</span></span></span><span class="line"><span class="cl"><span class="s1">}&#39;</span></span></span></code></pre></div>
</div>
<p>The <code>1</code> is the <code>pk</code> from the provider call&rsquo;s response. Both then appear in the overview:</p>
<p><figure class="my-6"><img src="/en/tutorials/authentik-connect-apps/anwendungen_hu_a190df419a72cb9a.webp" srcset="/en/tutorials/authentik-connect-apps/anwendungen_hu_9665cd701d1edb24.webp 480w, /en/tutorials/authentik-connect-apps/anwendungen_hu_a190df419a72cb9a.webp 768w, /en/tutorials/authentik-connect-apps/anwendungen_hu_ed38cdef55e354e4.webp 1200w, /en/tutorials/authentik-connect-apps/anwendungen_hu_662069adcbe5865e.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/authentik-connect-apps/anwendungen_hu_3b3ccfcd2ace5d3e.webp"
    alt="The application list in Authentik with two entries: Grafana through an OAuth2/OpenID provider and whoami through a proxy provider" title="Two applications, two routes: OIDC provider and proxy provider"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Two applications, two routes: OIDC provider and proxy provider</figcaption></figure></p>
<h3 id="3-arm-the-outpost--and-the-most-common-trap">3. Arm the outpost – and the most common trap</h3>
<p>A proxy provider does not work alone: an <strong>outpost</strong> serves it. Authentik ships an embedded outpost that
runs inside the <code>server</code> container – you only have to assign the provider to it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">OUT</span><span class="o">=</span><span class="k">$(</span>curl -s -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/outposts/instances/&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -c <span class="s2">&#34;import json,sys; print(json.load(sys.stdin)[&#39;results&#39;][0][&#39;pk&#39;])&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">curl -s -X PATCH -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/outposts/instances/</span><span class="nv">$OUT</span><span class="s2">/&#34;</span> -d <span class="s1">&#39;{&#34;providers&#34;: [1]}&#39;</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Without authentik_host your users land on localhost
  </p>
  <div class="prose-kitchen text-sm"><p>The embedded outpost does not know its public address by itself. In the test the <code>authentik_host</code> field
was <strong>empty</strong>, and an unauthenticated visitor was redirected to
<code>http://localhost/application/o/authorize/…</code> – a dead page for every user. Set it once and the redirect
is correct:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X PATCH -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/outposts/instances/</span><span class="nv">$OUT</span><span class="s2">/&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{&#34;config&#34;: {&#34;authentik_host&#34;: &#34;https://YOUR_DOMAIN/&#34;}}&#39;</span></span></span></code></pre></div>
</div>
<p>Afterwards the redirect points at <code>https://YOUR_DOMAIN/application/o/authorize/…</code> – measured before and
after. In the interface the same field lives under <strong>Applications → Outposts → Edit</strong>.</p>
</div>
</div>
<h3 id="4-traefik-the-forward-auth-middleware">4. Traefik: the forward auth middleware</h3>
<p>Now the connection is missing. Traefik asks the outpost before each request – that is what the
<code>forwardauth</code> middleware does. On the protected service:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">whoami</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">traefik/whoami:v1.12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.rule=Host(`YOUR_DOMAIN`) &amp;&amp; PathPrefix(`/whoami`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.middlewares=ak-auth&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.whoami.loadbalancer.server.port=80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.ak-auth.forwardauth.address=http://server:9000/outpost.goauthentik.io/auth/traefik&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.ak-auth.forwardauth.trustForwardHeader=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.ak-auth.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid&#34;</span></span></span></code></pre></div>
</div>
<p>The three decisive lines:</p>
<ul>
<li><strong><code>address</code></strong> points at the outpost – here through the service name <code>server</code> (Authentik reaches it under this alias on the Docker network) on the shared Docker
network, so <strong>without</strong> a detour over the internet. The path
<code>/outpost.goauthentik.io/auth/traefik</code> is the Traefik-specific check endpoint.</li>
<li><strong><code>trustForwardHeader=true</code></strong> passes the <code>X-Forwarded-*</code> headers along so Authentik knows the
originally requested address and can redirect back correctly.</li>
<li><strong><code>authResponseHeaders</code></strong> is the actual benefit: Traefik copies these headers from Authentik&rsquo;s response
into the request to your application. Without that line the service is protected but has no idea <strong>who</strong>
is there.</li>
</ul>
<h3 id="5-verify-that-it-really-protects">5. Verify that it really protects</h3>
<p>First without a login – <code>curl</code> is enough:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -o /dev/null -w <span class="s2">&#34;%{http_code} → %{redirect_url}\n&#34;</span> https://YOUR_DOMAIN/whoami</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">302 → https://YOUR_DOMAIN/application/o/authorize/?client_id=…&amp;redirect_uri=…%2Foutpost.goauthentik.io%2Fcallback…</span></span></code></pre></div>
</div>
<p>A <code>302</code> to the Authentik login is the desired result. In the browser the login page appears <strong>before</strong>
the service:</p>
<p><figure class="my-6"><img src="/en/tutorials/authentik-connect-apps/login-vor-whoami_hu_4e00c752777fda43.webp" srcset="/en/tutorials/authentik-connect-apps/login-vor-whoami_hu_308f3ef141065056.webp 480w, /en/tutorials/authentik-connect-apps/login-vor-whoami_hu_4e00c752777fda43.webp 768w, /en/tutorials/authentik-connect-apps/login-vor-whoami_hu_3ff6a74764fd1bf3.webp 1200w, /en/tutorials/authentik-connect-apps/login-vor-whoami_hu_b4def24d72c9718c.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/authentik-connect-apps/login-vor-whoami_hu_ffdad5461985c78a.webp"
    alt="The Authentik login page shown when opening the protected service" title="Not signed in: Authentik asks before the service answers"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Not signed in: Authentik asks before the service answers</figcaption></figure></p>
<p>After logging in you are back at <code>/whoami</code> – and the service shows what it receives:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">GET /whoami HTTP/1.1
</span></span><span class="line"><span class="cl">Host: YOUR_DOMAIN
</span></span><span class="line"><span class="cl">X-Authentik-Email: you@YOUR_DOMAIN
</span></span><span class="line"><span class="cl">X-Authentik-Groups: authentik Admins
</span></span><span class="line"><span class="cl">X-Authentik-Name: authentik Default Admin
</span></span><span class="line"><span class="cl">X-Authentik-Uid: d216e191a6f972c39931a005a7998a68…
</span></span><span class="line"><span class="cl">X-Authentik-Username: akadmin
</span></span><span class="line"><span class="cl">X-Forwarded-Proto: https</span></span></code></pre></div>
</div>
<p>That is the whole trick: the application has not a single line of login code – and still knows who is
sitting in front of it. Services that support header authentication (Grafana with <code>auth.proxy</code>,
Paperless, Forgejo with reverse-proxy auth) can turn that straight into a session.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Headers are only as safe as their path
  </p>
  <div class="prose-kitchen text-sm">If an application trusts <code>X-Authentik-Username</code>, that header must <strong>never</strong> be able to come from
outside. Hence: the service is reachable exclusively through Traefik (no published port), and the
middleware is attached to <strong>every</strong> router leading to it. A second router without the middleware is an
open door.</div>
</div>
<h3 id="6-know-the-limits-of-forward-auth">6. Know the limits of forward auth</h3>
<p>Before you put everything behind forward auth: the mechanism assumes a <strong>browser</strong>. An unauthenticated
request gets a <code>302</code> redirect to a login form – and that is exactly what breaks anything that is not a
browser:</p>
<ul>
<li><strong>Mobile apps</strong> (Nextcloud, Immich, Audiobookshelf) cannot do anything with the redirect. They see
HTML instead of JSON and report „server unreachable&quot;. For such services <strong>OIDC</strong> is the only sensible
route – or you exempt the app&rsquo;s paths deliberately.</li>
<li><strong>API access and scripts</strong> (<code>curl</code>, backup jobs, webhooks) run into nothing. Wherever a service is
addressed by token, that path has to bypass the middleware – or you move the protection to an
<a href="/en/tutorials/traefik-middlewares-hardening/">IP allowlist</a>.</li>
<li><strong>WebDAV and sync clients</strong> behave like APIs, with the same outcome.</li>
</ul>
<p>For such exceptions you create a <strong>second router</strong> without the auth middleware that serves only the
necessary path – and protect it differently, for example with the application&rsquo;s own token. Example for
an API path:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.app-api.rule=Host(`YOUR_DOMAIN`) &amp;&amp; PathPrefix(`/api`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.app-api.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.app-api.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.app-api.middlewares=rate@file&#34;</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm">Every such exception router is a deliberate hole in your protection. Keep the path as narrow as possible
(<code>/api</code>, not <code>/</code>), and write down why it exists – in six months nobody will remember otherwise.</div>
</div>
<p>For completeness, the third mode of the proxy provider: <strong><code>forward_domain</code></strong>. With it <strong>one</strong> outpost
serves all services of a domain (<code>*.YOUR_DOMAIN</code>), and users sign in once for all of them. That is the
more convenient setup from roughly three services onwards, but it requires Authentik and all
applications to live under the same parent domain so the session cookie can be shared. What is tested
here is <code>forward_single</code> – switching only affects the provider&rsquo;s mode and the cookie domain.</p>
<h3 id="7-route-b-openid-connect-for-services-with-their-own-sso">7. Route B: OpenID Connect for services with their own SSO</h3>
<p>If the application has an OIDC login, that is the better route: it then keeps real user accounts
including roles instead of just seeing a name in a header.</p>
<p>Create the provider – what matters is the <strong>redirect URI</strong>, which must match the application&rsquo;s callback
address exactly (Grafana as an example):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/providers/oauth2/&#34;</span> -d <span class="s2">&#34;{
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;name\&#34;: \&#34;grafana-oidc\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;authorization_flow\&#34;: \&#34;</span><span class="nv">$AUTHZ</span><span class="s2">\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;invalidation_flow\&#34;: \&#34;</span><span class="nv">$INVAL</span><span class="s2">\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;client_type\&#34;: \&#34;confidential\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;redirect_uris\&#34;: [{\&#34;matching_mode\&#34;: \&#34;strict\&#34;, \&#34;url\&#34;: \&#34;https://GRAFANA_DOMAIN/login/generic_oauth\&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s2">}&#34;</span></span></span></code></pre></div>
</div>
<p>The response contains <code>client_id</code> and <code>client_secret</code> – you need both in the application. Then create an
<strong>application</strong> pointing at this provider, as above.</p>
<p>Whether everything lines up is revealed by the discovery document. Authentik publishes it per
application under its slug:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://YOUR_DOMAIN/application/o/grafana/.well-known/openid-configuration <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -m json.tool <span class="p">|</span> head -12</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&#34;issuer&#34;: &#34;https://YOUR_DOMAIN/application/o/grafana/&#34;,
</span></span><span class="line"><span class="cl">&#34;authorization_endpoint&#34;: &#34;https://YOUR_DOMAIN/application/o/authorize/&#34;,
</span></span><span class="line"><span class="cl">&#34;token_endpoint&#34;: &#34;https://YOUR_DOMAIN/application/o/token/&#34;,
</span></span><span class="line"><span class="cl">&#34;userinfo_endpoint&#34;: &#34;https://YOUR_DOMAIN/application/o/userinfo/&#34;,</span></span></code></pre></div>
</div>
<p>Note: the <strong>issuer is application-specific</strong> (it contains the slug), while the remaining endpoints are the
same for all applications. Exactly these addresses – or the discovery URL itself if the app supports it –
go into the application. For Grafana, for instance:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_ENABLED</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_NAME</span><span class="o">=</span><span class="s">Authentik</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_CLIENT_ID</span><span class="o">=</span><span class="s">YOUR_CLIENT_ID</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET</span><span class="o">=</span><span class="s">YOUR_CLIENT_SECRET</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_SCOPES</span><span class="o">=</span><span class="s">openid profile email</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_AUTH_URL</span><span class="o">=</span><span class="s">https://YOUR_DOMAIN/application/o/authorize/</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_TOKEN_URL</span><span class="o">=</span><span class="s">https://YOUR_DOMAIN/application/o/token/</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_API_URL</span><span class="o">=</span><span class="s">https://YOUR_DOMAIN/application/o/userinfo/</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Groups as roles
  </p>
  <div class="prose-kitchen text-sm">Most applications can derive roles from a field in the token – Grafana for example via
<code>role_attribute_path</code>. That lets you steer admin rights centrally through Authentik groups instead of
maintaining them in every app. Which is exactly why part 1 advised attaching permissions to <strong>groups</strong>
rather than individual users.</div>
</div>
<h3 id="8-who-may-do-what-restricting-access">8. Who may do what? Restricting access</h3>
<p>As long as <strong>no</strong> policy is attached to an application, every signed-in user may use it. For the normal
case you bind a group to the application: <strong>Applications → your application → policy / group / user
bindings</strong>, then pick the group.</p>
<p>Only after that does the tile disappear from the user library for everyone else – and access is blocked.
The most honest way to verify is with a second test account that is <strong>not</strong> in the group: it should
neither see nor be able to open the application.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The script aborts while looking up the flows with <code>IndexError: list index out of range</code>.</strong> This
only happens right after a fresh installation: <code>/-/health/ready/</code> already reports <code>200</code> while the
<code>worker</code> is still importing the bundled flows. In our test there was only one flow at that moment,
all 15 were there 20 to 30 seconds later. Wait briefly and run it again.</p>
<p><strong>The request redirects to <code>http://localhost/application/o/authorize/…</code>.</strong> The embedded outpost does not
know its public address: the <code>authentik_host</code> field is empty. Set it to <code>https://YOUR_DOMAIN/</code> (step 3) –
before that every user lands on a dead page.</p>
<p><strong>Traefik answers with 500, or no redirect happens at all.</strong> Then the middleware cannot reach the
outpost. The <code>address</code> has to be resolvable from Traefik&rsquo;s point of view – so the <strong>container name</strong> on
the shared network (<code>http://server:9000/outpost.goauthentik.io/auth/traefik</code>), not <code>localhost</code>. From
the Traefik folder, check with <code>docker compose exec traefik wget -qO- http://server:9000/-/health/live/</code>
whether the connection works.</p>
<p><strong>The service is protected but does not know the user.</strong> <code>authResponseHeaders</code> is missing from the
middleware. Without that list Traefik discards the identity headers from Authentik&rsquo;s response – the login
works, but the application sees nothing.</p>
<p><strong>After signing in you end up in a redirect loop.</strong> Usually <code>external_host</code> does not match the address
actually being called (http instead of https, with or without a subdomain). It has to be exactly the URL
users open the service at.</p>
<p><strong>The OIDC login fails with „redirect_uri mismatch&quot;.</strong> Authentik checks the callback address strictly.
Enter exactly the URI the application sends – for Grafana e.g.
<code>https://GRAFANA_DOMAIN/login/generic_oauth</code>. A missing trailing slash is enough to break it.</p>
<p><strong>All signed-in users get in everywhere.</strong> Without a binding to a group an application is open to every
authenticated user. Attach a group binding to the application (step 8) and verify with an account that
is not in the group.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Your problem is not listed?
  <a href="/en/errors/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Search all error messages →</a>
</p>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p><strong>Spot-check after every Authentik update.</strong> An update can change flows and providers; the integrations
themselves are robust, but the quick test is worth it: <code>curl</code> on the protected path must still return
<code>302</code>, and the discovery document must show your endpoints. Two commands that keep a login from being
silently broken for weeks.</p>
<p><strong>Client secrets belong in the password manager.</strong> They live in the application (environment variables)
<strong>and</strong> in Authentik. If one is regenerated, the other side has to follow – note down which app uses
which secret when you create it, otherwise you will be searching in both systems later.</p>
<p><strong>The database matters even more now.</strong> Providers, applications and group bindings all live in
PostgreSQL. The <code>pg_dump</code> from <a href="/en/tutorials/authentik-single-sign-on/">part 1</a> is no longer a
nice-to-have: without it you rebuild every integration after an outage. Back it up daily with
<a href="/en/tutorials/restic-backups/">Restic</a>.</p>
<p><strong>Keep the emergency exit open.</strong> Every service behind forward auth is only as reachable as Authentik.
Keep a local account in important applications – and put <strong>no</strong> tools behind SSO that you need for
repairs; that applies especially to <a href="/en/tutorials/dockge-manage-compose/">Dockge</a> and similar management
interfaces. <a href="/en/tutorials/uptime-kuma-monitoring/">Monitoring</a> on <code>/-/health/ready/</code> notices the outage
before your users do.</p>
<p><strong>Tidy up tokens.</strong> The bootstrap token from step 1 is an admin access without a second factor. Once the
setup is in place, delete it under <strong>Directory → Tokens</strong> – and create a dedicated token with limited
rights for later automation.</p>
]]></content:encoded></item></channel></rss>