feat: add OAuth2 (XOAUTH2) authentication for IMAP#147
Open
sebykrueger wants to merge 1 commit into
Open
Conversation
Adds a Google XOAUTH2 path alongside the existing password authentication for users on Workspace tenants that disable App Passwords (or who prefer revocable OAuth grants). Existing password configs continue to work unchanged. Implementation - Nested `imap.auth` block in IMAPConfig with discriminator (`type: password|xoauth2`); implicit fallback to password when the block is absent and `password` is set. - New internal/imap/oauth package: Google provider, RFC 8252 loopback redirect flow with PKCE S256, RFC 6750 SASL XOAUTH2 client (the emersion/go-sasl library only ships OAUTHBEARER, which Microsoft doesn't accept over IMAP). - `--oauth-login` CLI flag runs the loopback flow on a host with a browser; refresh token written to `<dirname(database.path)>/secrets.json` (mode 0600) when the path is writable, otherwise printed for use via the `IMAP_OAUTH_REFRESH_TOKEN` env override. - Token caching via stock `oauth2.ReuseTokenSource` β access tokens stay in memory across fetch cycles within a single daemon run. - New `parse_dmarc_imap_auth_required` Prometheus gauge: set to 1 on terminal auth errors (`invalid_grant`, 4xx) so operators can alert when a refresh token requires re-bootstrap. Provider scope - Google only in v1. The provider abstraction is structured so Microsoft 365 / Entra ID is a one-file addition (different endpoints, scope = `https://outlook.office.com/IMAP.AccessAsUser.All offline_access`). - Hardcoded scope `https://mail.google.com/` β the only Google scope that grants IMAP access. Narrower Gmail API scopes work via REST only. - Device authorization grant (RFC 8628) was evaluated and rejected: Google does not permit `mail.google.com` in the device-flow scope allowlist, so loopback is the only viable flow for headless servers (run --oauth-login on a host with a browser, then ship the refresh token to the daemon via env var or file). Docker - compose.yml gains `env_file: [.env]` with `required: false` so existing App Password users are unaffected. Tests - Config: backwards-compat (password-only), XOAUTH2 validation, unknown provider/auth-type rejection, secrets-path co-location. - OAuth: secrets round-trip with 0600 perms + env override, terminal vs transient error classification, PKCE verifier/challenge correctness, XOAUTH2 wire format.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
imap.authblock with a discriminator field (type: password|xoauth2); existing password-only configs validate and run unchanged via implicit fallback.internal/imap/oauthpackage implements the Google provider, RFC 8252 loopback flow with PKCE S256, and a SASL XOAUTH2 client (theemersion/go-sasllibrary only ships OAUTHBEARER, which Microsoft 365 doesn't accept over IMAP β so XOAUTH2 keeps the door open for the M365 follow-up).--oauth-loginruns the device-side bootstrap on a host with a browser, then the daemon runs fully headless using a refresh token from<dirname(database.path)>/secrets.jsonor theIMAP_OAUTH_REFRESH_TOKENenv override.parse_dmarc_imap_auth_requiredPrometheus gauge fires on terminal auth errors (invalid_grant, 4xx) so operators can alert when re-auth is needed.Why loopback flow, not device flow?
The device authorization grant (RFC 8628) was the first design β it works on truly headless servers. But Google does not permit
https://mail.google.com/in the device-flow scope allowlist, so it's structurally unusable for Gmail IMAP. Loopback flow is the standard recommendation (RFC 8252 Β§3.1.1) for native/CLI tools βrclone,mbsync,mutt_oauth2.py, andgmailctlall use the same pattern.For Docker users this means: run
--oauth-loginonce on a host with a browser, paste the printedIMAP_OAUTH_REFRESH_TOKEN=...line into a.envfile, then the container is fully headless. Thecompose.ymlis updated to includeenv_file: [.env, required: false]so existing App Password users are unaffected.Provider scope
Google only in v1. The provider abstraction is structured so Microsoft 365 / Entra ID is essentially a one-file addition (different OAuth endpoints, scope =
https://outlook.office.com/IMAP.AccessAsUser.All offline_access, same XOAUTH2 SASL mechanism).Test plan
go test ./...β config backwards-compat (password-only validates), XOAUTH2 validation (missing client_id/secret rejected, unknown provider rejected), secrets file round-trip with 0600 permissions and env override precedence, terminal vs transient OAuth error classification, PKCE verifier/challenge match, XOAUTH2 wire formatgo vet ./...cleango build ./...clean (CGO and non-CGO)--oauth-login(browser opened, consent succeeded, refresh token printed), setIMAP_OAUTH_REFRESH_TOKENin.env, randocker compose up -d, daemon connected toimap.gmail.com:993and authenticated successfully via XOAUTH2authblock defaults to absent β password mode)