snappymail/plugins/sso-auth
Markus Mauch 5c2ce1ae0f sso-auth: improve README with missing setup gotchas
- Explain why error_page 401 =200 is required (nginx preserves original
  status code without the override, causing fetch() to reject the body)
- Explain why {"Result":true} for POST avoids error dialogs
- Explain proxy_set_header placement: must be inside location / block,
  not server block (nginx silently drops parent-level headers when
  location block sets its own)
- Explain secfetch_allow = "site=same-site": Chromium tracks redirect
  initiator through the chain, so the final /?sso&hash= request carries
  Sec-Fetch-Site: same-site instead of same-origin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 11:08:04 +01:00
..
index.php Add sso-auth plugin: transparent SSO via reverse proxy header 2026-03-23 10:55:57 +01:00
README.md sso-auth: improve README with missing setup gotchas 2026-03-23 11:08:04 +01:00

SnappyMail SSO Auth

Transparent single sign-on for SnappyMail via a trusted reverse proxy that sets an HTTP header with the authenticated user's e-mail address.

Works with any forward-auth proxy: Authelia, Caddy, Traefik, Nginx (auth_request), or any other solution that sets a Remote-Email-style header after authentication.

How it differs from proxy-auth

proxy-auth sso-auth
IMAP credentials Requires a master user account on the IMAP server Uses each user's own credentials
IMAP server requirement Must support master user / impersonation Any standard IMAP server
First login Fully automatic User enters own password once
Subsequent logins Fully automatic Fully automatic

Use sso-auth when you cannot or do not want to configure a master user on your IMAP server.

Flow

Browser → Reverse Proxy → SnappyMail
              ↓
    Sets Remote-Email header
              ↓
    Plugin reads header
              ↓
    Credentials stored? ──yes──→ Auto-login (no dialog)
              ↓ no
    Show login form (e-mail pre-filled)
              ↓
    User enters IMAP password
              ↓
    Credentials stored encrypted
              ↓
    Future visits: fully automatic

Setup

1. Reverse proxy

Configure your proxy to authenticate the user and set the Remote-Email header. Example for Authelia with Nginx Proxy Manager:

auth_request /internal/authelia/authz;
auth_request_set $redirection_url $upstream_http_location;
auth_request_set $email $upstream_http_remote_email;

# Return JSON for XHR requests when session expires, redirect for page navigation
error_page 401 =200 @authelia_error;
location @authelia_error {
    default_type application/json;
    if ($request_method = POST) {
        return 200 '{"Result":true}';
    }
    return 302 $redirection_url;
}

location / {
    proxy_set_header Remote-Email $email;
    # ...
}

Note on error_page 401 =200: The =200 override is required. Without it nginx preserves the original 401 status code even when return 200 is used inside the named location, causing SnappyMail's fetch() to reject the response before reading the body.

Note on {"Result":true} for POST: When a SnappyMail XHR request arrives with an expired proxy session, returning this response causes SnappyMail to call location.reload(), which triggers a full page reload. The page load is then intercepted by the proxy (401 → redirect to login). This avoids error dialogs in the UI.

Note on proxy_set_header placement: The proxy_set_header directives must be inside the location / block, not in the server block. Nginx completely discards any proxy_set_header directives inherited from a parent block the moment the location block defines its own — so headers set at the server level would silently be dropped.

2. SnappyMail — application.ini

If your SSO provider and SnappyMail are on different subdomains (e.g. auth.example.com and mail.example.com), you must allow Sec-Fetch-Site: same-site in SnappyMail:

[security]
secfetch_allow = "site=same-site"

Why: When the SSO provider redirects back to SnappyMail, Chromium-based browsers track the original request initiator through the entire redirect chain. The final /?sso&hash= request therefore carries Sec-Fetch-Site: same-site instead of same-origin. SnappyMail's default security policy rejects this and returns "Access Denied" — the setting above allows it.

For logout, set a custom logout link pointing to your SSO provider. The rd (or redirect_uri) parameter tells the SSO provider where to send the user after logout:

[labs]
custom_logout_link = "https://auth.example.com/logout?rd=https://mail.example.com"

3. Activate the plugin

In the SnappyMail admin panel → Extensions → enable SSO Auth.

Configure:

Setting Description Example
Redirect URL Where to redirect when the SSO header is absent. Leave empty to show the normal login dialog. https://auth.example.com
Header name HTTP header set by the proxy with the user's e-mail address. Remote-Email

Credential storage

Each user's IMAP password is stored encrypted in:

APP_PRIVATE_DATA/storage/_sso_/{sanitized-email}/primary.json

The password is encrypted with APP_SALT + SSO-email using SnappyMail's built-in SnappyMail\Crypt class. No plaintext credentials are stored.

Additional accounts

Users can add extra IMAP accounts (e.g. a Gmail account) via the standard SnappyMail account switcher. The plugin does not interfere with additional account management.

Session tracking

The plugin sets a _sso_user cookie (SHA-1 of the SSO e-mail) to track which identity owns the current session. If the proxy reports a different user on the next page load (e.g. after an overnight session where another family member logged in), the old session is cleared and the new user is logged in automatically.

Loop protection

A _sso_pending cookie (120 s TTL) is set immediately before the /?sso&hash= redirect. If auto-login fails (e.g. wrong stored password), the cookie prevents an infinite redirect loop and the login form is shown instead so the user can re-enter their password.