Skip to content

Commit 99d9575

Browse files
committed
feat(auth): add multi-org login
1 parent 4176448 commit 99d9575

44 files changed

Lines changed: 1592 additions & 258 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
# Changelog
22

3-
## Unreleased
3+
## 0.9.0 - 2026-01-22
4+
5+
### Highlights
6+
7+
- Auth: multi-org login with per-client OAuth credentials + token isolation. (#96)
48

59
### Added
610

7-
- Chat: spaces, messages, threads, and DM commands (Workspace only). (#84) — thanks @salmonumbrella.
8-
- People: profile lookup, directory search, and relations commands. (#84) — thanks @salmonumbrella.
911
- Calendar: show event timezone and local times; add --weekday output. (#92) — thanks @salmonumbrella.
1012
- Gmail: show thread message count in search output. (#99) — thanks @jeanregisser.
1113
- Gmail: message-level search with optional body decoding. (#88) — thanks @mbelinky.
1214

1315
### Fixed
1416

1517
- Auth: fix Gmail search example in auth success template. (#89) — thanks @rvben.
16-
- Chat: normalize thread IDs and show a clearer error for consumer accounts. (#84)
1718
- CLI: remove redundant newlines in text output for calendar, chat, Gmail, and groups commands. (#91) — thanks @salmonumbrella.
1819
- Gmail: include primary account display name in send From header when available. (#93) — thanks @salmonumbrella.
1920
- Keyring: persist OAuth tokens across Homebrew upgrades. (#94) — thanks @salmonumbrella.
@@ -22,6 +23,17 @@
2223
- Calendar: force custom reminders payload to send UseDefault=false. (#100) — thanks @salmonumbrella.
2324
- Gmail: add read alias + default thread get. (#103) — thanks @salmonumbrella.
2425

26+
## 0.8.0 - 2026-01-19
27+
28+
### Added
29+
30+
- Chat: spaces, messages, threads, and DM commands (Workspace only). (#84) — thanks @salmonumbrella.
31+
- People: profile lookup, directory search, and relations commands. (#84) — thanks @salmonumbrella.
32+
33+
### Fixed
34+
35+
- Chat: normalize thread IDs and show a clearer error for consumer accounts. (#84)
36+
2537
## 0.7.0 - 2026-01-17
2638

2739
### Highlights

README.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ Before adding an account, create OAuth2 credentials from Google Cloud Console:
8888
gog auth credentials ~/Downloads/client_secret_....json
8989
```
9090

91+
For multiple OAuth clients/projects:
92+
93+
```bash
94+
gog --client work auth credentials ~/Downloads/work-client.json
95+
gog auth credentials list
96+
```
97+
9198
### 3. Authorize Your Account
9299

93100
```bash
@@ -109,7 +116,7 @@ gog gmail labels list
109116

110117
`gog` stores your OAuth refresh tokens in a “keyring” backend. Default is `auto` (best available backend for your OS/environment).
111118

112-
Before you can run `gog auth add`, you must store OAuth client credentials once via `gog auth credentials <credentials.json>` (download a Desktop app OAuth client JSON from the Cloud Console).
119+
Before you can run `gog auth add`, you must store OAuth client credentials once via `gog auth credentials <credentials.json>` (download a Desktop app OAuth client JSON from the Cloud Console). For multiple clients, use `gog --client <name> auth credentials ...`; tokens are isolated per client.
113120

114121
List accounts:
115122

@@ -131,6 +138,52 @@ Show current auth state/services for the active account:
131138
gog auth status
132139
```
133140

141+
### Multiple OAuth clients
142+
143+
Use `--client` (or `GOG_CLIENT`) to select a named OAuth client:
144+
145+
```bash
146+
gog --client work auth credentials ~/Downloads/work.json
147+
gog --client work auth add [email protected]
148+
```
149+
150+
Optional domain mapping for auto-selection:
151+
152+
```bash
153+
gog --client work auth credentials ~/Downloads/work.json --domain example.com
154+
```
155+
156+
How it works:
157+
158+
- Default client is `default` (stored in `credentials.json`).
159+
- Named clients are stored as `credentials-<client>.json`.
160+
- Tokens are isolated per client (`token:<client>:<email>`); defaults are per client too.
161+
162+
Client selection order (when `--client` is not set):
163+
164+
1) `--client` / `GOG_CLIENT`
165+
2) `account_clients` config (email -> client)
166+
3) `client_domains` config (domain -> client)
167+
4) Credentials file named after the email domain (`credentials-example.com.json`)
168+
5) `default`
169+
170+
Config example (JSON5):
171+
172+
```json5
173+
{
174+
account_clients: { "[email protected]": "work" },
175+
client_domains: { "example.com": "work" },
176+
}
177+
```
178+
179+
List stored credentials:
180+
181+
```bash
182+
gog auth credentials list
183+
```
184+
185+
See `docs/auth-clients.md` for the full client selection and mapping rules.
186+
134187
### Keyring backend: Keychain vs encrypted file
135188

136189
Backends:
@@ -317,6 +370,7 @@ gog keep get <noteId> --account [email protected]
317370
### Environment Variables
318371

319372
- `GOG_ACCOUNT` - Default account email or alias to use (avoids repeating `--account`; otherwise uses keyring default or a single stored token)
373+
- `GOG_CLIENT` - OAuth client name (selects stored credentials + token bucket)
320374
- `GOG_JSON` - Default JSON output
321375
- `GOG_PLAIN` - Default plain output
322376
- `GOG_COLOR` - Color mode: `auto` (default), `always`, or `never`
@@ -346,6 +400,14 @@ Example (JSON5 supports comments and trailing commas):
346400
347401
personal: "[email protected]",
348402
},
403+
// Optional per-account OAuth client selection
404+
account_clients: {
405+
"[email protected]": "work",
406+
},
407+
// Optional domain -> client mapping
408+
client_domains: {
409+
"example.com": "work",
410+
},
349411
}
350412
```
351413

@@ -423,6 +485,8 @@ Flag aliases:
423485

424486
```bash
425487
gog auth credentials <path> # Store OAuth client credentials
488+
gog auth credentials list # List stored OAuth client credentials
489+
gog --client work auth credentials <path> # Store named OAuth client credentials
426490
gog auth add <email> # Authorize and store refresh token
427491
gog auth service-account set <email> --key <path> # Configure service account impersonation (Workspace only)
428492
gog auth service-account status <email> # Show service account status
@@ -1247,6 +1311,7 @@ Opt-in tests that hit real Google APIs using your stored `gog` credentials/token
12471311
```bash
12481312
# Optional: override which account to use
12491313
1314+
export GOG_CLIENT=work
12501315
go test -tags=integration ./...
12511316
```
12521317

@@ -1259,11 +1324,13 @@ Fast end-to-end smoke checks against live APIs:
12591324
```bash
12601325
scripts/live-test.sh --fast
12611326
scripts/live-test.sh --account [email protected] --skip groups,keep,calendar-enterprise
1327+
scripts/live-test.sh --client work --account [email protected]
12621328
```
12631329

12641330
Script toggles:
12651331

12661332
- `--auth all,groups` to re-auth before running
1333+
- `--client <name>` to select OAuth client credentials
12671334
- `--strict` to fail on optional features (groups/keep/enterprise)
12681335
- `--allow-nontest` to override the test-account guardrail
12691336

docs/auth-clients.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# OAuth Clients
2+
3+
Use multiple OAuth client credentials (for different Google Cloud projects or brands) without mixing refresh tokens.
4+
5+
## How it works
6+
7+
- Default client name: `default`
8+
- Default credentials file: `$(os.UserConfigDir())/gogcli/credentials.json`
9+
- Named credentials files: `$(os.UserConfigDir())/gogcli/credentials-<client>.json`
10+
- Tokens are stored per client (`token:<client>:<email>`). Default client also writes legacy keys for backwards compatibility.
11+
- Default account is stored per client, with a legacy global fallback for the default client.
12+
13+
## Selecting a client
14+
15+
Use `--client` (or `GOG_CLIENT`) to pick which credentials + token bucket to use:
16+
17+
```
18+
gog --client work auth credentials ~/Downloads/work-client.json
19+
gog --client work auth add [email protected]
20+
gog --client work gmail search "is:unread"
21+
```
22+
23+
When `--client` is not set, `gog` resolves the client in this order:
24+
25+
1) `--client` / `GOG_CLIENT` override
26+
2) `account_clients` map in config
27+
3) `client_domains` map in config
28+
4) Credentials file named after the email domain (e.g. `credentials-example.com.json`)
29+
5) `default`
30+
31+
## Domain auto-map
32+
33+
To auto-select a client for a domain:
34+
35+
```
36+
gog --client work auth credentials ~/Downloads/work.json --domain example.com
37+
```
38+
39+
This writes `client_domains` into `config.json` so any `@example.com` account selects the `work` client.
40+
41+
## Listing stored credentials
42+
43+
```
44+
gog auth credentials list
45+
```
46+
47+
Shows stored credential files plus any configured domain mappings.
48+
49+
## Config example
50+
51+
```
52+
{
53+
keyring_backend: "auto",
54+
account_clients: {
55+
"[email protected]": "work",
56+
},
57+
client_domains: {
58+
"example.com": "work",
59+
},
60+
}
61+
```
62+
63+
## Migration notes
64+
65+
- Legacy `token:<email>` entries are copied to `token:default:<email>` the first time they are read.
66+
- Legacy `default_account` is still respected for the default client.

docs/spec.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ Implementation: `internal/ui/ui.go`.
6565
### OAuth client credentials (non-secret-ish)
6666

6767
- Stored on disk in the per-user config directory:
68-
- `$(os.UserConfigDir())/gogcli/credentials.json`
68+
- `$(os.UserConfigDir())/gogcli/credentials.json` (default client)
69+
- `$(os.UserConfigDir())/gogcli/credentials-<client>.json` (named clients)
6970
- Written with mode `0600`.
7071
- Command:
7172
- `gog auth credentials <credentials.json>`
73+
- `gog --client <name> auth credentials <credentials.json>`
74+
- `gog auth credentials list`
7275
- Supports Google’s downloaded JSON format:
7376
- `installed.client_id/client_secret` or `web.client_id/client_secret`
7477

@@ -78,7 +81,8 @@ Implementation: `internal/config/*`.
7881

7982
- Stored in OS credential store via `github.com/99designs/keyring`.
8083
- Key namespace is `gogcli` (keyring `ServiceName`).
81-
- Key format: `token:<email>`
84+
- Key format: `token:<client>:<email>` (default client uses `token:default:<email>`)
85+
- Legacy key format: `token:<email>` (migrated on first read)
8286
- Stored payload is JSON (refresh token + metadata like selected services/scopes).
8387
- Fallback: if no OS credential store is available, keyring may use its encrypted "file" backend:
8488
- Directory: `$(os.UserConfigDir())/gogcli/keyring/` (one file per key)
@@ -111,7 +115,8 @@ Scope selection note:
111115
- Base config dir: `$(os.UserConfigDir())/gogcli/`
112116
- Files:
113117
- `config.json` (JSON5; comments and trailing commas allowed)
114-
- `credentials.json` (OAuth client id/secret)
118+
- `credentials.json` (OAuth client id/secret; default client)
119+
- `credentials-<client>.json` (OAuth client id/secret; named clients)
115120
- State:
116121
- `state/gmail-watch/<account>.json` (Gmail watch state)
117122
- Secrets:
@@ -122,13 +127,15 @@ We intentionally avoid storing refresh tokens in plain JSON on disk.
122127
Environment:
123128

124129
- `[email protected]` (email or alias; used when `--account` is not set; otherwise uses keyring default or a single stored token)
130+
- `GOG_CLIENT=work` (select OAuth client bucket; see `--client`)
125131
- `GOG_KEYRING_PASSWORD=...` (used when keyring falls back to encrypted file backend in non-interactive environments)
126132
- `GOG_KEYRING_BACKEND={auto|keychain|file}` (force backend; use `file` to avoid Keychain prompts and pair with `GOG_KEYRING_PASSWORD` for non-interactive)
127133
- `GOG_TIMEZONE=America/New_York` (default output timezone; IANA name or `UTC`; `local` forces local timezone)
128134
- `GOG_ENABLE_COMMANDS=calendar,tasks` (optional allowlist of top-level commands)
129135
- `config.json` can also set `keyring_backend` (JSON5; env vars take precedence)
130136
- `config.json` can also set `default_timezone` (IANA name or `UTC`)
131137
- `config.json` can also set `account_aliases` for `gog auth alias` (JSON5)
138+
- `config.json` can also set `account_clients` (email -> client) and `client_domains` (domain -> client)
132139

133140
Flag aliases:
134141
- `--out` also accepts `--output`.
@@ -139,6 +146,8 @@ Flag aliases:
139146
### Implemented
140147

141148
- `gog auth credentials <credentials.json|->`
149+
- `gog auth credentials list`
150+
- `gog --client <name> auth credentials <credentials.json|->`
142151
- `gog auth add <email> [--services user|all|gmail,calendar,classroom,drive,docs,contacts,tasks,sheets,people,groups] [--readonly] [--drive-scope full|readonly|file] [--manual] [--force-consent]`
143152
- `gog auth services [--markdown]`
144153
- `gog auth keep <email> --key <service-account.json>` (Google Keep; Workspace only)
@@ -297,6 +306,8 @@ Flag aliases:
297306

298307
- `gog auth …`
299308
- `gog auth credentials <credentials.json>`
309+
- `gog auth credentials list`
310+
- `gog --client <name> auth credentials <credentials.json>`
300311
- `gog gmail …`
301312
- `gog chat …`
302313
- `gog calendar …`
@@ -400,10 +411,11 @@ Commands:
400411
There is an opt-in integration test suite guarded by build tags (not run in CI).
401412

402413
- Requires:
403-
- stored `credentials.json` via `gog auth credentials ...`
414+
- stored `credentials.json` (or `credentials-<client>.json`) via `gog auth credentials ...`
404415
- refresh token in keyring via `gog auth add <email>`
405416
- Run:
406417
- `[email protected] go test -tags=integration ./internal/integration`
418+
- optional: `GOG_CLIENT=work` to select a non-default OAuth client
407419

408420
## CI (GitHub Actions)
409421

internal/authclient/authclient.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package authclient
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/steipete/gogcli/internal/config"
9+
)
10+
11+
type contextKey struct{}
12+
13+
func WithClient(ctx context.Context, client string) context.Context {
14+
client = strings.TrimSpace(client)
15+
if client == "" {
16+
return ctx
17+
}
18+
19+
return context.WithValue(ctx, contextKey{}, client)
20+
}
21+
22+
func ClientOverrideFromContext(ctx context.Context) string {
23+
if ctx == nil {
24+
return ""
25+
}
26+
27+
if v := ctx.Value(contextKey{}); v != nil {
28+
if s, ok := v.(string); ok {
29+
return s
30+
}
31+
}
32+
33+
return ""
34+
}
35+
36+
func ResolveClient(ctx context.Context, email string) (string, error) {
37+
cfg, err := config.ReadConfig()
38+
if err != nil {
39+
return "", fmt.Errorf("read config: %w", err)
40+
}
41+
override := ClientOverrideFromContext(ctx)
42+
43+
client, err := config.ResolveClientForAccount(cfg, email, override)
44+
if err != nil {
45+
return "", fmt.Errorf("resolve client: %w", err)
46+
}
47+
48+
return client, nil
49+
}
50+
51+
func ResolveClientWithOverride(email string, override string) (string, error) {
52+
cfg, err := config.ReadConfig()
53+
if err != nil {
54+
return "", fmt.Errorf("read config: %w", err)
55+
}
56+
57+
client, err := config.ResolveClientForAccount(cfg, email, override)
58+
if err != nil {
59+
return "", fmt.Errorf("resolve client: %w", err)
60+
}
61+
62+
return client, nil
63+
}

0 commit comments

Comments
 (0)