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
|
Can we please close this PR? A reverse proxy that handles OIDC as a sidecar container is perfectly doable and the right architectural choice, as it keeps Maiplit's code base simple. I certainly don't want Mailpit to end like MailHog. |
|
I started looking into this today but got sidetracked by an urgent CVE that needed immediate attention. My apologies for the delay. I'd like to share some observations and concerns based on what I've reviewed so far. On multi-user authentication: I want to clarify that Mailpit's basic auth isn't limited to single users. The auth file can contain multiple unique user entries, so the multi-login capability you mentioned as a limitation isn't actually a constraint. I'm not suggesting it's as elegant as your solution, but it does address the staff turnover concern about password resets. On the OIDC integration feature: I appreciate the contribution genuinely, but I have significant reservations that I need to raise. My main concerns:The OIDC mechanics are unclear to me, which creates a real support problem. I'm the person users will contact when something breaks or won't authenticate. Without understanding how this works, I can't effectively troubleshoot issues or guide users through setup. Security risk. I've been focused on hardening Mailpit's attack surface, and integrating with external authentication systems likely expands that surface in ways I don't fully understand. Every feature that ships becomes a long-term maintenance obligation - I can't support something I don't comprehend, and I shouldn't have to rely on AI to maintain it. Complexity beyond the demo. Your demo made it look straightforward - just two environment variables - but I suspect that real-world OIDC integration is rarely that simple. Different providers would likely use different security algorithms, key exchange methods, and configuration requirements etc. Supporting all the listed OIDC services would likely require many more options and edge cases than the current implementation suggests. Alternatives exist. As noted, projects like oauth2-proxy already handle OIDC at the proxy layer, which keeps authentication concerns separate from Mailpit itself. I'm not saying no yet, but these are significant long-term risks that need to be understood before I can confidently make a decision. I'm interested in your thoughts about this @pgbezerra? As @D-i-t-gh stated, there are apparently already existing proxy-based solutions to achieve the same thing without requiring the functionality be added directly into Mailpit. |
We also use similar reverse proxy with AWS ELB for our Auth dependencies as mentioned by @D-i-t-gh . I agree with @axllent In current form this project is our go-to solution because it is lean, introducing these additional features would likely mean additional vetting, something I would like to avoid. |
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.