feat(auth): add OIDC login for the web UI#684
Conversation
Optional. Set MP_UI_OIDC_ISSUER and MP_UI_OIDC_CLIENT_ID to turn it on. When unset, behaviour is identical to today. The UI redirects to the IdP via Authorization Code + PKCE (handled in the SPA by oidc-client-ts). Tokens live in sessionStorage and are silently renewed via the refresh token. Each request carries the ID token as a Bearer JWT; the server verifies it against the IdP's JWKS (fetched once on boot and cached for 24h). Basic Auth still works in parallel so API integrations don't break. The oidc-client-ts bundle is only loaded when OIDC is enabled — nothing extra is shipped to users on default deployments.
- Let the SPA shell (HTML/JS/CSS, /search, /view/<id>, /auth/callback) load without an auth challenge when OIDC is enabled, so the SPA can run the redirect itself. API and direct .html/.txt previews stay gated. - Suppress WWW-Authenticate: Basic on 401s when OIDC is on, so the browser's native Basic dialog doesn't pop up on a SPA-side 401. Basic Auth still works for clients that send the header proactively. - Add the IdP origin to connect-src in the CSP so the SPA can reach the discovery / JWKS / token endpoints. - Drop the duplicate Basic Auth check in websockets.ServeWs — auth is the middleware's job, and the duplicate broke browser WS upgrades (browsers can't send Authorization on WS handshakes). - Move the SPA-side OIDC config to a cached Promise so callers race- free await the same init. AuthCallbackView does a hard navigation after exchange so the router guard sees the freshly stored user. - Store tokens in localStorage so a new tab inherits the session. - Move the logout button next to "About Mailpit" and show the signed- in user's name beside it.
603ee47 to
f2aa36f
Compare
|
Thanks @pgbezerra - this will likely take me some time to review properly, especially since it appears to be fully AI‑written (Am I wrong?) and it’s a fairly large code addition. I have several other changes I need to push out first, including some security fixes, so I’ll need to handle those before coming back to this. I’m sure I’ll have more questions once I dig into it, but it may be about a week before I can make a start. |
|
Just my thoughts: |
Thanks for that, @axllent! There is one thing I noticed and I haven't fixed, which is a function in Besides reviewing the code and iterating to not open an AI slop PR, I also tested all authentication flows and showed that in the YouTube video I posted above. |
I thought about adding a reverse proxy as well, but don't you think this is more work than adding a client to the existing IdP, and two env vars in MailPit? That was why I checked the code to see how hard it would be to implement this change directly in Mailpit. My understanding was that it doesn't increase code complexity much, and the frontend bundle gets ~30k larger only if OIDC is enabled, which should not impact performance for those who don't have it enabled. |
|
@axllent, do you want me to rebase and fix the |
|
@pgbezerra - feel free to rebase it if you like (there were a large number of changes recently for the v1.30.0 release). I am very time-poor at the moment, so have not started on this yet - and I'm not 100% sure when I will unfortunately. I know I had said about a week (about a week ago), but I got 4 CVE reports late last week which took priority, and I needed to get the changes out. I definitely do plan to review this - hopefully within the next week or two - but I will need to spend a fairly considerable amount of time to fully understand the implications of these changes, as well as how OIDC works exactly. Sorry about the delay, but I have to be sure this is a good fit/feature, and it's a huge code change (addition). As I always say to contributors, it's not that the work wasn't done "for me", it's that I have to continue to maintain it in the future. |
Totally understandable @axllent. Appreciate your quick answer, and good luck on the CVEs. In two weeks, I'll double-check whether I need to rebase it again and ping you then, if that's okay. |
# Conflicts: # go.mod # go.sum
Demo
https://youtu.be/hwBq5VjvEx0
Summary
Adds optional OIDC (Authorization Code + PKCE) login for the web UI, while keeping HTTP Basic Auth working for API clients and integrations.
Closes #683.
Why
Sharing one Basic Auth password across a team is awkward — every time someone leaves, the secret has to be rotated and redistributed. Putting the UI behind a corporate OIDC IdP (Keycloak, Okta, Google, Auth0, …) lets organisations gate Mailpit behind SSO without losing the simple Basic Auth path their automations rely on.
How to turn it on
Set two env vars (or the equivalent flags):
When unset, behaviour is byte-for-byte identical to today.
Design notes
oidc-client-ts. No client secret on the server.Authorization: Bearer <id_token>. The server verifies it against the IdP's JWKS (fetched once on boot, cached for 24h).offline_access. Users don't get bounced to the IdP mid-session.Authorization: Basic …keeps being checked against the htpasswd store — Send API, scripts, scrapers don't change.localStorage, so new tabs in the same browser inherit the session.oidc-client-tslives in its own entry point (dist/oidc-entry.js) that the HTML template only includes when OIDC is enabled — default deployments ship zero extra bytes.What's tested
internal/auth/oidc_test.gocovering: disabled / missing client ID / unreachable issuer / JWKS preload on boot / happy-path verify / Bearer-prefix stripping / wrong audience / wrong issuer / expired / bad signature / empty + disabled / JWKS cached across verifies / JWKS refresh after TTL.server/server_test.gocovering: valid Bearer → 200, expired / tampered Bearer → 401, Bearer in?access_token=query → 200 (for WebSocket auth), SPA shell loads without auth challenge, OIDC + Basic coexist, OIDC off ⇒ unchanged Basic behaviour, both nil ⇒ anonymous.Things intentionally not changed
sendAPIAuthMiddlewareand the Send API path — already auth-isolated, integrations keep using Basic Auth or--send-api-auth-accept-any.livez/readyz.config.Webrootconvention.connect-srcentry for the IdP origin), gzip, theskipUIAuthKeycontext flag.