Skip to content

Commit 603ee47

Browse files
committed
docs: add OIDC setup guide
Adds OIDC.md at the repo root with: env-var/flag reference, the redirect URI to register with the IdP, a minimal Keycloak docker compose example to try it locally, notes on Basic Auth coexistence, webroot, CSP, and the lack of any allowlist by design. Links it from the README's authentication bullet.
1 parent f2aa36f commit 603ee47

2 files changed

Lines changed: 192 additions & 1 deletion

File tree

OIDC.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# OIDC login for the web UI
2+
3+
Mailpit can put the web UI behind a corporate OIDC IdP (Keycloak, Okta,
4+
Google, Auth0, Authentik, …) so a shared team instance is gated by SSO
5+
instead of one shared Basic Auth password.
6+
7+
OIDC is **opt-in**. When the issuer is unset, behaviour is unchanged —
8+
the UI is open or guarded by Basic Auth exactly as before.
9+
10+
Basic Auth keeps working alongside OIDC, so the Send API and any
11+
scripts / integrations that already use `Authorization: Basic …` don't
12+
need to change.
13+
14+
## Configure Mailpit
15+
16+
Set two values (CLI flags or env vars):
17+
18+
| Flag | Env var | What it is |
19+
|-------------------------|--------------------------|-----------------------------------------------|
20+
| `--ui-oidc-issuer` | `MP_UI_OIDC_ISSUER` | Issuer URL (the OIDC discovery endpoint root) |
21+
| `--ui-oidc-client-id` | `MP_UI_OIDC_CLIENT_ID` | Client ID registered with the IdP |
22+
23+
Example:
24+
25+
```shell
26+
MP_UI_OIDC_ISSUER=https://idp.example.com/realms/myrealm \
27+
MP_UI_OIDC_CLIENT_ID=mailpit \
28+
mailpit
29+
```
30+
31+
On boot, Mailpit fetches the IdP's discovery document and JWKS once
32+
and caches them for 24h. A failure here is fatal — a typo in the
33+
issuer URL or an unreachable IdP causes startup to fail with a clear
34+
error, so misconfiguration is loud.
35+
36+
## Configure your IdP
37+
38+
Register Mailpit as a **public** client with **Authorization Code +
39+
PKCE** and a single redirect URI:
40+
41+
```
42+
<scheme>://<mailpit-host>:<port><webroot>auth/callback
43+
```
44+
45+
For a default local install: `http://localhost:8025/auth/callback`.
46+
If `MP_WEBROOT=/mailpit/` is set, it becomes
47+
`http://localhost:8025/mailpit/auth/callback`.
48+
49+
Recommended client settings:
50+
51+
- Public client (no client secret)
52+
- Standard flow (Authorization Code) **enabled**
53+
- Implicit / Direct Access Grants / Service Accounts: **disabled**
54+
- PKCE method: `S256`
55+
- Scopes requested by the SPA: `openid email profile offline_access`
56+
(the `offline_access` scope must be allowed so refresh-token rotation
57+
works and users aren't bounced back to the IdP mid-session)
58+
- Post-logout redirect URI: the Mailpit root, e.g. `http://localhost:8025/`
59+
60+
No client secret is needed on the Mailpit side — the SPA drives the
61+
PKCE dance directly with the IdP. Mailpit only needs the issuer URL
62+
to verify incoming Bearer JWTs against the IdP's JWKS.
63+
64+
## How it works (one-screen overview)
65+
66+
- The SPA redirects unauthenticated users to the IdP via PKCE and
67+
exchanges the auth code for tokens client-side.
68+
- Tokens live in `localStorage` (so new tabs inherit the session) and
69+
are silently renewed via the refresh token — no hidden iframes, no
70+
third-party cookies.
71+
- Every API call carries `Authorization: Bearer <id_token>`. The
72+
WebSocket (`/api/events`) carries the same token as
73+
`?access_token=…` because browsers can't set headers on WS upgrades.
74+
- The server verifies each JWT against the IdP's JWKS (cached for 24h
75+
or until restart).
76+
- Basic Auth is still honoured for any request that proactively sends
77+
it, so API integrations continue to work.
78+
79+
No state is kept on the server: no cookies, no session store, no HMAC
80+
secret.
81+
82+
## Try it locally with Keycloak
83+
84+
A throwaway Keycloak instance is enough to exercise the full flow.
85+
86+
`docker-compose.yml`:
87+
88+
```yaml
89+
services:
90+
keycloak:
91+
image: quay.io/keycloak/keycloak:26.0
92+
command: ["start-dev", "--import-realm"]
93+
environment:
94+
KC_BOOTSTRAP_ADMIN_USERNAME: admin
95+
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
96+
KC_HTTP_ENABLED: "true"
97+
KC_HOSTNAME: "http://localhost:8080"
98+
KC_HOSTNAME_STRICT: "false"
99+
ports:
100+
- "8080:8080"
101+
volumes:
102+
- ./keycloak-realm.json:/opt/keycloak/data/import/mailpit-realm.json:ro
103+
104+
mailpit:
105+
image: axllent/mailpit:latest
106+
# Host network so Mailpit reaches Keycloak at the same hostname
107+
# the browser uses (localhost:8080). The token's `iss` claim and
108+
# the configured issuer URL must match exactly.
109+
network_mode: host
110+
environment:
111+
MP_UI_OIDC_ISSUER: http://localhost:8080/realms/mailpit
112+
MP_UI_OIDC_CLIENT_ID: mailpit
113+
depends_on:
114+
- keycloak
115+
```
116+
117+
`keycloak-realm.json`:
118+
119+
```json
120+
{
121+
"realm": "mailpit",
122+
"enabled": true,
123+
"loginWithEmailAllowed": true,
124+
"users": [
125+
{
126+
"username": "test@example.com",
127+
"email": "test@example.com",
128+
"emailVerified": true,
129+
"enabled": true,
130+
"credentials": [
131+
{ "type": "password", "value": "test", "temporary": false }
132+
],
133+
"realmRoles": ["default-roles-mailpit", "offline_access"]
134+
}
135+
],
136+
"clients": [
137+
{
138+
"clientId": "mailpit",
139+
"enabled": true,
140+
"publicClient": true,
141+
"standardFlowEnabled": true,
142+
"directAccessGrantsEnabled": false,
143+
"implicitFlowEnabled": false,
144+
"protocol": "openid-connect",
145+
"redirectUris": ["http://localhost:8025/auth/callback"],
146+
"webOrigins": ["+"],
147+
"attributes": {
148+
"pkce.code.challenge.method": "S256",
149+
"post.logout.redirect.uris": "http://localhost:8025/"
150+
},
151+
"optionalClientScopes": ["offline_access"]
152+
}
153+
]
154+
}
155+
```
156+
157+
Then:
158+
159+
```shell
160+
docker compose up
161+
```
162+
163+
Open <http://localhost:8025>, log in with `test@example.com` / `test`,
164+
and you should land in the inbox. The user's name shows in the
165+
sidebar next to a sign-out button.
166+
167+
## Coexistence with Basic Auth
168+
169+
If both `MP_UI_AUTH` (htpasswd) and OIDC are configured:
170+
171+
- The browser SPA always goes through OIDC.
172+
- `curl -u user:pass …/api/v1/messages` and other clients that send
173+
`Authorization: Basic …` still work — Mailpit accepts either.
174+
- A 401 from an unauthenticated request returns
175+
`X-Mp-Auth-Required: oidc` and does **not** include a
176+
`WWW-Authenticate: Basic` header, so the browser never pops up its
177+
native Basic-Auth dialog on a SPA-side 401.
178+
179+
## Notes & limits
180+
181+
- **No allowlist of any kind.** Anyone the IdP authenticates is
182+
allowed in. Restrict who reaches the IdP itself (groups, federation,
183+
etc.) instead.
184+
- **Webroot.** All flows follow `MP_WEBROOT` — adjust the redirect URI
185+
in the IdP accordingly.
186+
- **CSP.** Mailpit's Content Security Policy is automatically extended
187+
with the IdP's origin in `connect-src` so the SPA can reach the
188+
discovery, JWKS, and token endpoints.
189+
- **Send API.** Unchanged. It has its own auth path
190+
(`--send-api-auth` or `--send-api-auth-accept-any`) and is not
191+
affected by the UI's OIDC config.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Mailpit was originally **inspired** by MailHog which is [no longer maintained](h
3434

3535
- Runs entirely from a single [static binary](https://mailpit.axllent.org/docs/install/) or multi-architecture [Docker images](https://mailpit.axllent.org/docs/install/docker/)
3636
- Modern web UI with advanced [mail search](https://mailpit.axllent.org/docs/usage/search-filters/) to view emails (formatted HTML, highlighted HTML source, text, headers, raw source, and MIME attachments
37-
including image thumbnails), including optional [HTTPS](https://mailpit.axllent.org/docs/configuration/http/) & [authentication](https://mailpit.axllent.org/docs/configuration/http/)
37+
including image thumbnails), including optional [HTTPS](https://mailpit.axllent.org/docs/configuration/http/), [Basic Auth](https://mailpit.axllent.org/docs/configuration/http/) & [OIDC login](OIDC.md) (SSO for the web UI)
3838
- [SMTP server](https://mailpit.axllent.org/docs/configuration/smtp/) with optional STARTTLS or SSL/TLS, authentication (including an "accept any" mode)
3939
- A [REST API](https://mailpit.axllent.org/docs/api-v1/) for integration testing
4040
- Real-time web UI updates using web sockets for new mail & optional [browser notifications](https://mailpit.axllent.org/docs/usage/notifications/) when new mail is received

0 commit comments

Comments
 (0)