One Login to Rule Them All: Wiring Authelia as SSO for Open WebUI and Nextcloud

Here’s the thing nobody tells you when you start adding Authelia to a homelab: “SSO” is not one integration pattern. It’s two, and they don’t look anything alike under the hood, even though from the browser they feel identical — type your username, tap your TOTP code, land on the app. Same login screen, same muscle memory, same warm feeling of having Solved Authentication Forever. (If you haven’t actually turned on real TOTP yet and are still running password-only, Locking Down Authelia

Under the hood, one of my apps has no idea Authelia exists. The other one talks to it directly, as a first-class citizen, using a protocol with its own handshake, its own tokens, its own opinions about what a “user” even is. Get this distinction wrong — or worse, not realize it’s a distinction at all — and you’ll spend an evening debugging a problem that looks like a bug but is actually a category error. I know this because I did exactly that a few weeks later, but that’s a story for a different post.

For today: how do you actually wire Authelia in front of Open WebUI and Nextcloud, and why are these two setups fundamentally different shapes, not just different flavors of the same thing?

The app that doesn’t know it’s protected

Open WebUI, like a lot of self-hosted tools, was never built with an external identity provider in mind. It has its own login form, its own user table, its own idea of “logged in.” If I want Authelia sitting in front of it, I’m not integrating with Open WebUI’s authentication — I’m overriding it from the outside, before any of its own login logic ever runs.

This is forward-auth, and the mental model is simpler than the name suggests: Caddy intercepts every request to openwebui.yourdomain.com and, before it reverse-proxies anything to Open WebUI, makes a side-channel call to Authelia asking one question — “is this person allowed in?” If Authelia says yes, Caddy lets the request through to Open WebUI exactly as if nothing happened. If Authelia says no, the visitor gets redirected to a login page, and Open WebUI never even sees the request.

The forward-auth block in my Caddyfile looks roughly like this:

caddyfile

(forward_auth_block) {
  forward_auth authelia:9091 {
    uri /api/verify?rd=https://authelia.yourdomain.com
    copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
  }
}

openwebui.yourdomain.com:80 {
  import forward_auth_block
  reverse_proxy openwebui:8080
}

Two things matter here that are easy to skim past. First, copy_headers — once Authelia approves the request, it hands back a set of headers identifying who just logged in, and Caddy stitches those onto the request before it reaches Open WebUI. Second, and this is the part that actually matters for day-to-day reliability: Open WebUI still has no idea any of this happened. It sees a request arrive, optionally with some Remote-User-style headers it may or may not even read, and that’s it. As far as Open WebUI’s own code is concerned, Authelia doesn’t exist.

That’s the whole trick of forward-auth. It’s a bouncer standing in front of a door, not a doorman the building hired. The building’s own systems are completely unaware there’s anyone checking IDs outside. This is also exactly why it works for literally any app — Open WebUI, a dashboard, an internal wiki, whatever — regardless of whether that app has ever heard of OIDC, SAML, or any authentication standard at all. If it speaks HTTP, you can put a forward-auth gate in front of it. (This is the exact same forward_auth_block reused later, almost verbatim, to gate a self-hosted SearXNG instance behind Authelia too.)

The tradeoff is just as structural. Because the protected app doesn’t know about Authelia, it also doesn’t really know who’s logged in beyond whatever you choose to pass through those headers. There’s no rich user object, no group claims baked into a token the app can inspect on its own, no session the app controls. Everything the app “knows” about the user is whatever you handed it in a header, and if that header mapping is wrong or missing, the app just falls back to whatever its own default behavior is — which, for Open WebUI, meant new accounts landing in a “pending” status that I had to manually promote the first time around. The app isn’t broken; it’s just genuinely in the dark about the bigger picture.

The app that wants to know you personally

Nextcloud is a different animal entirely, because user_oidc isn’t a gate in front of Nextcloud — it’s a plugin running inside Nextcloud that speaks OpenID Connect directly to Authelia. There’s no bouncer here. Nextcloud itself walks up to Authelia, introduces itself with a client ID and secret, and asks for a proper, structured answer: who is this person, what’s their email, what groups are they in, and here’s a signed token to prove you’re not lying to me.

This is OIDC done the way the spec actually intends it, and it shows in how much more Nextcloud knows once the dance is over. Setting it up means configuring an actual client relationship between the two systems, not just a header-passing convention:

bash

docker exec -u www-data nextcloud php occ user_oidc:provider authelia \
  --clientid="nextcloud" \
  --clientsecret="..." \
  --discoveryuri="https://authelia.yourdomain.com/.well-known/openid-configuration"

That discovery URI is doing a lot of quiet work. Nextcloud fetches it once, learns where Authelia’s authorization endpoint, token endpoint, and signing keys all live, and from then on the two systems are talking a real protocol with real cryptographic guarantees — not just trusting whatever headers happen to show up. When I log in through Nextcloud’s web UI, I’m redirected to Authelia, I authenticate there (TOTP and all), and Authelia hands back a signed JWT containing claims: email, groups, name, sub. Nextcloud reads that token directly and uses it to provision or update my account.

This is the crucial difference. Open WebUI never sees a token — it sees headers Caddy chose to forward. Nextcloud sees an actual cryptographically signed identity document and parses it itself. That gives Nextcloud a much richer, more reliable picture of who’s logging in — group membership for access control, email for notifications, display name for the UI — all sourced directly from the identity provider rather than relayed secondhand through a proxy’s header rewriting.

It also means Nextcloud has opinions about how it builds a user from that token, and those opinions matter more than they look like they should. By default, user_oidc uses the sub claim — a stable, unique identifier Authelia generates — as the internal Nextcloud account ID. That’s the technically correct choice for uniqueness, and it’s also a UUID, not anything resembling a username. I didn’t think about this at all when I set it up, because why would I — the login worked, I could see my files, case closed.

It came back to bite me weeks later in a completely different context (ask me about the desktop sync client some other time), but the shape of the problem is worth flagging here: with forward-auth, the protected app never had to make a decision about identity at all, because it was never given the opportunity. With native OIDC, the protected app is making real decisions — how to map claims to accounts, what becomes the username, whether to provision automatically or require a pre-existing account — and every one of those decisions is a place a default can surprise you.

Same login screen, two different trust relationships

Step back far enough and the distinction is really about where the trust boundary sits. With forward-auth, the trust boundary is at the edge — Caddy and Authelia have a private conversation, and the app behind them is simply handed a verdict. The app trusts Caddy to have done its job; it has no independent way to verify anything, and it doesn’t need one, because it was never designed to.

With native OIDC, the trust boundary moves inside the app itself. Nextcloud doesn’t trust Caddy’s opinion about who’s logged in — Caddy, in fact, is barely involved in the authentication conversation at all beyond passing bytes back and forth. Nextcloud independently verifies a cryptographic token against keys it fetched from Authelia’s discovery document. It’s a direct relationship between the two systems, with the reverse proxy reduced to plumbing.

Neither pattern is strictly better — they solve different problems. Forward-auth is the right tool when you want to blanket-protect something that was never built with SSO in mind, and you’re willing to accept that the protected app will only ever know what you choose to tell it. Native OIDC is the right tool when the app actually needs to reason about identity — assign permissions by group, send mail to the right address, distinguish “logged in as Marco” from “logged in as some anonymous header value” — and you’re willing to take on the deeper integration work and the protocol-level decisions that come with it.

What I’d tell past-me, a few weeks before the desktop sync client incident: when you wire up native OIDC, go look at exactly what claim becomes the account ID, on purpose, before you start using the account for anything real. It’s a five-minute check during setup. It’s a multi-hour debugging session if you skip it and only find out later, indirectly, through a symptom that looks like it’s about something else entirely.

Same login screen. Two completely different conversations happening behind it. Worth knowing which one you’re having before you need to debug it.

This same forward-auth-versus-native-protocol distinction is worth keeping in mind for the network-scoped Authelia bypass too — Open WebUI’s web-search calls to SearXNG hit an API directly, with no forward-auth bouncer able to intercept them the normal way, which is exactly why that post needed a different kind of trust rule instead.

Leave a Comment

Your email address will not be published. Required fields are marked *