imap-oidc-bridge — SSO Against Mailbox-Only Webhosting


I run a few small things — a club, a couple of side projects — that all share the same shape. There are 5–30 users, they already have email at the project’s domain, the email is on a Hetzner Webhosting plan because that’s what €5/month buys you, and at some point you want to put real software in front of them: a Nextcloud, a WordPress, a Vaultwarden. With one set of credentials. Across all of them.

The polite way to do this is single sign-on. The honest way is “let everyone use whatever password they already use for their mail.”

Those are the same thing if your auth proxy can plug into your mailbox host. Mine can’t.

The Problem

Authentik (and Keycloak, and Dex, and everything else in the SSO category) expects a source: LDAP, OIDC, SAML, SCIM, Kerberos. Hetzner Webhosting publishes none of those. Their control panel, KonsoleH, has no API and never has — there’s a single GitHub project from 2014 that scrapes the UI with PhantomJS and explicitly says “KonsoleH does not actually have an API.” PhantomJS has been dead since 2018.

So your Authentik can’t see the users. Your users can’t reuse their mail password. You end up either:

  • Maintaining a parallel user list in Authentik (painful, drifts immediately)
  • Running a fake LDAP server that does an IMAP LOGIN on every BIND (a real hack people have built; brittle, no group sync, no enumeration)
  • Forking Authentik to add an IMAP source type (not a thing I’m doing)

None of those are good. So I wrote a fourth option.

What I Built

imap-oidc-bridge is a ~570-line FastAPI service that exposes the standard OIDC endpoints — /.well-known/openid-configuration, /jwks.json, /authorize, /token, /userinfo — and on the back of /authorize does exactly one thing: an IMAP LOGIN against the IMAP server you configure. If that succeeds, the bridge mints an RS256-signed ID token with sub=email and email_verified=true. If it fails, the form re-renders with an error.

Authentik (or Keycloak, or anything that consumes OIDC) sees a perfectly normal OpenID Connect identity provider. It has no idea that on the other side of the bridge there’s an IMAP4 socket doing the actual credential check. Users see a login page, type their mail password, and end up logged into Nextcloud.

The bridge is stateless apart from a generated RSA keypair persisted to disk. There’s no user database. There’s no password storage. There’s no group attribute (IMAP can’t tell you any of that). Group assignment in your OIDC consumer is policy-based — “anyone who comes in via this source belongs to group X.” That’s enough for a small group.

What I Didn’t Build

  • A user directory. You can’t enumerate users via IMAP. You manage them in your control panel. The bridge knows what the user typed and what IMAP said about it; nothing more.
  • A password store. The bridge never persists credentials. Each login revalidates against IMAP.
  • An MFA layer. Authentik does this beautifully and the bridge sits in front of it — layer your TOTP / WebAuthn / whatever in the consumer’s flow, not here.
  • Token revocation. OIDC tokens stay valid until OIDC_TOKEN_TTL expires (default 1h, drop it to 5 minutes if deletion semantics matter). No persistent revocation list. The bridge is meant to be small enough to read in one sitting.

A Real Bug, Found Honestly

I tested the bridge against a real Mailcow with a multi-account drive script — five throwaway mailboxes, six passes (concurrent baseline, rolling password rotation, IMAP unreachable, mixed-batch isolation, definitely-broken creds, unknown user). Most of it worked first try. One thing didn’t: when Mailcow’s Dovecot was slow to authenticate (it does a live LDAP bind to Authentik for every IMAP LOGIN, and five parallel attempts were enough to push past the 8-second default timeout), the bridge propagated TimeoutError straight to FastAPI as a 500. Fail-open-visibly, leaking internal state.

The fix was three lines in imap.py — catch TimeoutError and OSError during conn.login(), wrap as IMAPAuthError, return 401. The interesting thing wasn’t the fix; it was that the bug only showed up when I drove the bridge against a real backend under realistic load. Mock IMAP in unit tests will never tell you that. I’d missed it for two days.

Open Source

github.com/scharc/imap-oidc-bridge — MIT, alpha. The README has a Hetzner-Webhosting-shaped quickstart, an Authentik integration walkthrough, and a copy-pasteable blueprint. Source-only for now: build the Docker image yourself, point it at your IMAP host, register it as an OAuth source in Authentik, and you have SSO for users you don’t manage.

If you have a small-club shape and you’ve been working around a missing IMAP source in your IdP, give it a try.