-
Notifications
You must be signed in to change notification settings - Fork 118
Description
Context
When deploying Twake Mail against a third-party JMAP server (e.g. Stalwart) that delegates authentication to an external OIDC identity provider (e.g. Kanidm, Keycloak, Authentik), the OIDC issuer lives on a completely different origin than the JMAP server.
Example topology:
| Component | URL |
|---|---|
| JMAP server (Stalwart) | https://mail.example.org |
| OIDC Identity Provider (Kanidm) | https://id.example.org |
| Twake Mail web | https://webmail.example.org |
The JMAP server validates Bearer tokens issued by the IdP (via its userinfo endpoint), but does not serve /.well-known/openid-configuration itself — that endpoint lives on the IdP.
Current behavior
Twake Mail assumes the OIDC authority can be discovered from the JMAP server URL:
LoginController._checkOIDCIsAvailable()sends a WebFinger / OIDC discovery request to theSERVER_URLorigin.- Since the JMAP server does not expose
/.well-known/openid-configuration, the request returns 404 or times out. - The app falls back to basic auth, which may not be available (the server only accepts Bearer tokens via OIDC).
This means there is currently no way to use Twake Mail with a JMAP server whose OIDC provider is on a different domain, unless the JMAP server proxies the OIDC discovery endpoint (which is unusual and creates coupling).
Expected behavior
Allow operators to configure the OIDC authority independently from the JMAP server URL via env.file:
SERVER_URL=https://mail.example.org
AUTHORITY_OIDC=https://id.example.org/oauth2/openid/tmail-web
WEB_OIDC_CLIENT_ID=tmail-web
OIDC_SCOPES=openid,profile,email
DOMAIN_REDIRECT_URL=https://webmail.example.orgWhen AUTHORITY_OIDC is set:
- Skip WebFinger/OIDC discovery against
SERVER_URLentirely. - Build the
OIDCConfigurationdirectly from the configured authority, client ID, and scopes. - Proceed with the standard Authorization Code + PKCE flow against the configured IdP.
- Present the resulting access token as a Bearer token to the JMAP server.
When AUTHORITY_OIDC is not set, the current WebFinger discovery behavior is preserved (no breaking change).
Authenticated flow (with AUTHORITY_OIDC)
┌────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Twake Mail │ │ OIDC IdP │ │ JMAP Server │
│ (browser) │ │ (Kanidm, etc.) │ │ (Stalwart) │
└─────┬──────┘ └────────┬─────────┘ └──────┬───────┘
│ │ │
│ 1. Read AUTHORITY_OIDC from env.file │
│ 2. Fetch .well-known/openid-configuration │
│ ──────────────────► │ │
│ 3. OIDC config │ │
│ ◄────────────────── │ │
│ │ │
│ 4. Redirect user to authorization endpoint│
│ ──────────────────► │ │
│ 5. User logs in │ │
│ 6. Auth code + redirect back │
│ ◄────────────────── │ │
│ │ │
│ 7. Exchange code for access_token (PKCE) │
│ ──────────────────► │ │
│ 8. access_token + refresh_token │
│ ◄────────────────── │ │
│ │ │
│ 9. JMAP requests with Bearer access_token │
│ ─────────────────────────────────────────► │
│ 10. Stalwart validates token via IdP │
│ userinfo endpoint │
│ │ ◄──────────────────── │
│ │ token introspection │
│ │ ────────────────────► │
│ 11. JMAP response │
│ ◄───────────────────────────────────────── │
Implementation notes
The changes required are minimal and fully backward-compatible. The key touch points are:
app_config.dart— exposeAUTHORITY_OIDCfromenv.filelogin_controller.dart— in_checkOIDCIsAvailable(), whenauthorityOidcis non-empty, directly constructOIDCConfigurationand skip WebFinger discoveryhandle_web_finger_to_get_token_extension.dart— same bypass for the token refresh / re-authentication pathoidc_configuration_cache_manager.dart— preferAUTHORITY_OIDCfrom env over the cached SharedPreferences value, so a config change takes effect without clearing app stateDockerfile— removeassets/env.filefrom the Flutter service worker cache manifest so that a volume-mountedenv.fileis always fetched fresh at runtime
We have a work-in-progress exploration of this approach in our fork: https://github.com/Deepthought-Solutions/tmail-flutter — the flow works end-to-end for the login path but we are still debugging edge cases (token refresh, re-authentication after expiry). Happy to collaborate on a PR if the approach looks acceptable.
Related
- [COMPANY SERVER] Allow to authenticate with Authorization BEARER #3455 — Bearer token auth for providers like Fastmail (complementary: that issue covers static app tokens; this covers dynamic OIDC tokens with an external IdP)
- Running Front End with OIDC Disabled Issue #4127 — OIDC discovery failing when backend has no OIDC (related: both issues stem from the assumption that
SERVER_URLalways serves OIDC discovery)