Support multiple OAuth apps for Google Workspace orgs#215
Conversation
Specifies backward-compatible config format, schema migration, CLI changes (add-account --oauth-app), and sync-time resolution for supporting multiple Google Workspace OAuth client secrets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add path normalization note for Apps map entries, clarify serve command scheduler integration, and note deletions scope handling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Token/binding mismatch: re-auth on oauth_app change instead of trying to detect client identity from token files 2. Serve fallback: fall back to default app when no source row exists 3. Default app non-mandatory: defer OAuth checks to per-account resolution, add HasAnyConfig helper Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Headless: PrintHeadlessInstructions must include --oauth-app in both browser-side and server-side commands 2. Sync bootstrap: unknown-email path falls back to default app; named-app-only configs require add-account first Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 tasks covering config, schema, CLI, sync commands, serve, verify, deletions, and documentation. Addresses reviewer feedback on IMAP nil-manager call site and deletions variable scoping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expand OAuthConfig with a named-app map (map[string]OAuthApp) and two helper methods: - ClientSecretsFor(name): returns default secrets path when name is empty, or the named app's path; errors with actionable messages. - HasAnyConfig(): true when any secrets path is configured. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend Load() to apply expandPath and resolveRelative to each named app's client_secrets path, matching the existing treatment of the top-level client_secrets. Adds tests covering tilde expansion, relative path resolution against the config dir, and named-apps-only configs with no default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add nullable oauth_app TEXT column to sources, wired through the Source struct, scanSource, all five SELECT queries, and a new UpdateSourceOAuthApp method. NULL means "use default OAuth app"; a non-null value maps to a named app in [oauth.apps.<name>] config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Supports named OAuth apps for Google Workspace orgs. Detects binding changes and re-authorizes when switching apps. Headless instructions include --oauth-app when specified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace single-manager pattern with lazy cache keyed by app name. Sync commands now resolve the correct OAuth credentials per account. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Look up source's oauth_app binding for each scheduled sync. Fall back to default app when no source row exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both commands now look up the source's oauth_app binding before creating an OAuth manager. Error messages mention named apps when configured. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. oauthManagerCache: add sync.Mutex for concurrent serve syncs 2. add-account rebind: authorize first, persist binding only on success (prevents broken state on cancelled auth) 3. Inherit stored oauth_app binding on re-add without --oauth-app (not just --force); support clearing binding back to default via explicit --oauth-app "" or cmd.Flags().Changed detection 4. subset.go: explicit column list instead of SELECT * to avoid breakage when copying from pre-oauth_app databases 5. verify/deletions: capture findGmailSource errors instead of silently falling back to default app 6. Hint message references add-account specifically instead of suggesting --oauth-app on commands that don't have it Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. add-account rebind: stop deleting token before auth succeeds. Authorize atomically writes the new token on success; if auth fails or is cancelled, the old token remains intact. 2. subset.go: try copying with oauth_app column first, fall back to NULL for source databases created before the column existed. Fixes CopySubset against pre-oauth_app databases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. TestAddAccount_RebindPreservesTokenOnFailure: verifies that when OAuth app rebinding fails (auth cancelled), the original token file and DB binding remain intact. 2. TestCopySubset_LegacySourceWithoutOAuthApp: verifies CopySubset succeeds when the source DB predates the oauth_app column, with NULL in the destination. 3. browserFlow: bail early if context is already cancelled to avoid starting an HTTP server or opening a browser. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Move the --headless early return below the DB lookup and binding inheritance, so re-running add-account --headless for a named-app account prints instructions with the correct --oauth-app flag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Headless only prints instructions, so DB errors should not block it. The DB lookup to inherit the stored oauth_app binding is now best-effort: if --oauth-app was provided explicitly, skip the lookup entirely; otherwise try to read the binding but fall back silently on any DB/schema/lookup error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Fixes: --headless --oauth-app "" would silently re-inherit the stored binding instead of clearing to default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Verifies that --headless --oauth-app "" does not re-inherit the stored binding and prints instructions without --oauth-app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR descriptions should be concise and changelog-oriented, not include test plans or implementation details. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
1. verify: call InitSchema() before source lookup so the oauth_app column exists when querying pre-migration databases. 2. add-account: when rebinding with an existing token, update the binding without forcing re-authorization. This makes headless rebind work: copy token to server, then run add-account --oauth-app to update the binding without needing a browser. 3. Tests updated: RebindWithExistingToken verifies binding update without auth, ForceRebindPreservesBindingOnFailure verifies --force with failed auth leaves old binding intact. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
When switching an account's OAuth app, the existing token may have been minted by the old app's client and won't refresh against the new one. Store the OAuth client_id in token file metadata and check it on rebind: - Token from same client (headless copy scenario): reuse it - Token from different client or legacy token without client_id: force re-authorization Also fix serve.go to propagate findGmailSource errors instead of silently falling back to the default app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
When --oauth-app is explicitly set and no source row exists yet, validate the existing token's client_id against the named app. A mismatched token (from a different OAuth client) is rejected instead of silently accepted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
The API's tokenFile struct was missing the client_id field, so tokens uploaded to a headless server had their client_id stripped. This broke the headless registration flow for named OAuth apps since add-account --oauth-app would reject the token as mismatched. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
roborev: Combined Review (
|
Including --oauth-app "" (clear to default). Previously the empty string skipped the client check on first-time registration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers both paths: mismatched token rejected, matching token accepted when explicitly clearing to default app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Prevents hanging on browser auth if regression makes the token non-reusable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Previously, re-running add-account on a named-app account without --oauth-app skipped client validation because the binding was inherited, not explicit. A stale token from a different client would be silently accepted and fail on next refresh. Now checks whenever resolvedApp is non-empty, regardless of source. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
roborev: Combined Review (
|
Table-driven test covers both paths: matching token reused, mismatched token rejected when binding is inherited from DB without explicit --oauth-app flag. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Tested this locally with a Google Workspace account and it's working well. I'll merge this soon and get a 0.11.0 release out |
roborev: Combined Review (
|
|
ok to merge as is |
Summary
--oauth-appflag toadd-accountfor binding accounts to named OAuth apps[oauth.apps.<name>]config sections alongside the existing default[oauth].client_secretsoauth_appcolumn tosourcestableCloses #201
Motivation
Some Google Workspace organizations require OAuth apps to live within their org. A personal OAuth app cannot authorize accounts in those orgs. This adds support for multiple named OAuth client secrets so users with accounts across different Workspace orgs can archive all of them.
Usage
🤖 Generated with Claude Code