Skip to content

Commit 88f2ea9

Browse files
docs: add Google Workspace SSO setup guide (#85)
Documents the Google Workspace SSO feature, which shipped without setup docs. Written while configuring it end-to-end on a real deployment, so it covers the parts that only surface when you actually try it. ## Why this is needed The four `SECURITY_GOOGLE_*` variables appear nowhere — not in `.env.example`, not in the README. Worse, **two required steps cannot be discovered from configuration at all**: - **The domain allowlist is a database table** (`google_workspace_domain`), not an environment variable. Until a row exists, SSO rejects every sign-in while looking fully configured. Nothing points you at the table. - **SSO does not provision accounts.** A valid Workspace user who was never added to DeepSQL gets `403 This account is not provisioned in DeepSQL`. That is the right behaviour — otherwise anyone in the org could grant themselves access to a tool holding production database credentials — but undocumented it reads as a bug. ## Two traps documented **`APP_PUBLIC_URL` vs `APP_BASE_URL`.** Both default to `http://localhost:3000` and they are not interchangeable: `app.public-url` drives every auth redirect *and* invite emails, `app.base-url` only the CLI device flow. Set the wrong one and sign-in succeeds, writes the session cookie correctly, then lands the user on `localhost:3000/dashboard` — a failure that looks like broken auth but is purely a redirect target. **Audience is project-wide.** Flipping an existing Cloud project from External to Internal cuts off every non-org user of *any other app* in that project. The guide says to check Overview → Metrics for live OAuth traffic first, and to prefer a separate project when in doubt. ## Contents `docs/GOOGLE_SSO_SETUP.md` covers: creating the client in Google Cloud Console (including the current **Google Auth Platform** layout, where the old single consent-screen page is now split across Audience / Branding / Clients / Data Access), why Internal is worth taking, sharing one project across multiple apps, the four variables, `APP_PUBLIC_URL`, the allowlist `INSERT`, restart and verification, provisioning, `SECURITY_PASSWORD_ENABLED`, and a troubleshooting table keyed by the exact error strings the app returns. Also adds the variables to `.env.example` with the two non-obvious caveats inline, and index entries in `README.md` and `docs/README.md`. ## Note There is no `gcloud` path for creating the OAuth client — `gcloud iap oauth-clients` was the nearest equivalent and was shut down in March 2026, and it never supported custom redirect URIs. The guide states this so nobody spends time looking for one. Docs only — no code changes. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent f1c03f3 commit 88f2ea9

4 files changed

Lines changed: 234 additions & 0 deletions

File tree

.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,28 @@ SLACK_BOT_TOKEN=
259259
SLACK_SIGNING_SECRET=
260260
SLACK_DEEPSQL_BOT_USERNAME=
261261

262+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
263+
# OPTIONAL — Google Workspace SSO
264+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
265+
# Full walkthrough: docs/GOOGLE_SSO_SETUP.md
266+
#
267+
# Two things that are easy to miss:
268+
# - The domain allowlist is a DATABASE TABLE (google_workspace_domain), not a
269+
# variable here. SSO rejects everyone until a row exists for your domain.
270+
# - SSO does not create accounts. Users must exist in DeepSQL first, or they
271+
# get "403 This account is not provisioned in DeepSQL".
272+
273+
SECURITY_GOOGLE_ENABLED=false
274+
SECURITY_GOOGLE_CLIENT_ID=
275+
SECURITY_GOOGLE_CLIENT_SECRET=
276+
# Must match the Authorized redirect URI on the OAuth client EXACTLY.
277+
SECURITY_GOOGLE_REDIRECT_URI=https://deepsql.example.com/api/auth/google/callback
278+
279+
# Set false to make SSO exclusive: hides the password form and refuses
280+
# /auth/login. Enabling SSO alone does NOT close the password path.
281+
# Everyone must be able to sign in with Google before you set this.
282+
SECURITY_PASSWORD_ENABLED=true
283+
262284
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
263285
# OPTIONAL — Admin Bootstrap (managed by install.sh)
264286
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ caching · nginx.
443443
- [`docs/README.md`](docs/README.md) — documentation index
444444
- [`AGENTS.md`](AGENTS.md) — codebase map
445445
- [`mcp/README.md`](mcp/README.md) — CLI and MCP server
446+
- [`docs/GOOGLE_SSO_SETUP.md`](docs/GOOGLE_SSO_SETUP.md) — Google Workspace SSO, and disabling password sign-in
446447
- [`SECURITY.md`](SECURITY.md) — reporting a vulnerability
447448

448449
## License

docs/GOOGLE_SSO_SETUP.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Google Workspace SSO
2+
3+
Let your team sign in to DeepSQL with their work Google accounts, and
4+
optionally turn off password sign-in entirely.
5+
6+
DeepSQL verifies Google's `hd` (hosted domain) claim against an allowlist you
7+
control, so only domains you name can get in — a personal Gmail account cannot
8+
sign in even if someone types a matching email address.
9+
10+
**Two things surprise most people, so they are called out up front:**
11+
12+
1. The allowlist is a **database table**. Configuring the environment variables
13+
alone leaves SSO rejecting everyone. See [step 4](#4-allowlist-your-domain).
14+
2. SSO does **not** create accounts. A user who has never been added to DeepSQL
15+
gets `403 This account is not provisioned in DeepSQL`, even with a valid
16+
Workspace login. That is deliberate — see [Provisioning](#provisioning-users).
17+
18+
---
19+
20+
## 1. Create the OAuth client
21+
22+
OAuth clients are created in **Google Cloud Console** (`console.cloud.google.com`),
23+
*not* in the Workspace Admin console (`admin.google.com`). There is no `gcloud`
24+
command for this — the IAP OAuth Admin API was the closest thing and was shut
25+
down in March 2026, and it never supported custom redirect URIs anyway.
26+
27+
In the Cloud Console, the relevant area is **Google Auth Platform**. It was
28+
previously a single "OAuth consent screen" page and is now split:
29+
30+
| You want | Where it lives now |
31+
| --- | --- |
32+
| Internal / External | **Audience** |
33+
| Create an OAuth client | **Clients** |
34+
| App name, logo, support email | **Branding** |
35+
| Scopes | **Data Access** |
36+
37+
Steps:
38+
39+
1. Pick or create a project. It must belong to your Google Workspace
40+
**organization**, or the Internal option in step 2 is not offered.
41+
2. **Branding** — app name, user support email, developer contact.
42+
3. **Audience** — choose **Internal**.
43+
4. **Clients → Create client**
44+
- Application type: **Web application**
45+
- Authorized redirect URI, exactly:
46+
`https://deepsql.example.com/api/auth/google/callback`
47+
- Leave *Authorized JavaScript origins* empty — this is a server-side flow.
48+
5. Copy the **Client ID** and **Client secret**.
49+
50+
### Choose Internal if you can
51+
52+
**Internal** means Google itself refuses to issue a token to anyone outside your
53+
organization — the request never reaches DeepSQL. That sits on top of DeepSQL's
54+
own domain allowlist, so a mistake in one does not open the door. It also avoids
55+
the "unverified app" warning that External shows.
56+
57+
> **Do not flip an existing project from External to Internal without checking
58+
> what already uses it.** The Audience setting is project-wide. If another
59+
> application in that project serves users outside your organization, switching
60+
> to Internal cuts them off immediately. Check **Overview → Metrics** for live
61+
> OAuth traffic first; if in doubt, create a separate project.
62+
63+
### One project can serve many apps
64+
65+
`Audience` and `Branding` are per-project; **Clients** are not. Add one client
66+
per application and they share the Internal gate with no extra setup.
67+
68+
Because Branding is shared, name it for the organization rather than one app —
69+
`Acme Internal` rather than `DeepSQL` — since every app in the project shows that
70+
same name on its consent screen.
71+
72+
---
73+
74+
## 2. Configure DeepSQL
75+
76+
```bash
77+
SECURITY_GOOGLE_ENABLED=true
78+
SECURITY_GOOGLE_CLIENT_ID=<client id>
79+
SECURITY_GOOGLE_CLIENT_SECRET=<client secret>
80+
SECURITY_GOOGLE_REDIRECT_URI=https://deepsql.example.com/api/auth/google/callback
81+
```
82+
83+
`SECURITY_GOOGLE_REDIRECT_URI` must match the value registered in the Cloud
84+
Console **character for character**, trailing slash included, or Google rejects
85+
the callback.
86+
87+
---
88+
89+
## 3. Set your public URL
90+
91+
```bash
92+
APP_PUBLIC_URL=https://deepsql.example.com
93+
```
94+
95+
**This one catches people.** There are two similarly named settings and they do
96+
different jobs:
97+
98+
| Variable | Property | Used for |
99+
| --- | --- | --- |
100+
| `APP_PUBLIC_URL` | `app.public-url` | **every auth redirect**, including the Google callback, and invite emails |
101+
| `APP_BASE_URL` | `app.base-url` | the `deepsql login` CLI device-flow authorize URL |
102+
103+
Both default to `http://localhost:3000`. Leave `APP_PUBLIC_URL` unset and Google
104+
sign-in appears to work, then dumps the user on `localhost:3000/dashboard` — the
105+
session cookie was written correctly, but the browser is sent nowhere useful.
106+
Set both on any real deployment.
107+
108+
---
109+
110+
## 4. Allowlist your domain
111+
112+
**SSO rejects every sign-in until you do this.** There is no environment
113+
variable for it; the allowlist lives in the `google_workspace_domain` table so
114+
it can be changed without a restart.
115+
116+
```sql
117+
INSERT INTO google_workspace_domain (domain, enabled, created_at, updated_at)
118+
VALUES ('example.com', true, NOW(), NOW());
119+
```
120+
121+
A sign-in is accepted only when the domain is present **and** enabled **and**
122+
Google's `hd` claim for that account matches it. Checking `hd` rather than the
123+
email suffix is what stops a personal account from impersonating a Workspace
124+
one.
125+
126+
To revoke a domain later, set `enabled = false` — it takes effect immediately.
127+
128+
---
129+
130+
## 5. Restart and verify
131+
132+
Configuration is read at startup, and `docker restart` does **not** re-read
133+
`.env`:
134+
135+
```bash
136+
docker compose up -d --force-recreate backend
137+
```
138+
139+
Check the flag is live:
140+
141+
```bash
142+
curl -s https://deepsql.example.com/api/setup/status
143+
# {"setupComplete":true, ... ,"googleEnabled":true,"passwordLoginEnabled":true}
144+
```
145+
146+
`googleEnabled: true` makes the **Sign in with Google** button appear on the
147+
login page.
148+
149+
---
150+
151+
## Provisioning users
152+
153+
Google proves *who someone is*. It does not decide *whether they may use
154+
DeepSQL*. A user who has never been added gets:
155+
156+
```
157+
403 This account is not provisioned in DeepSQL
158+
```
159+
160+
This is intentional. Without it, anyone in your Workspace — contractors,
161+
interns, every department — could grant themselves access to a tool holding
162+
production database credentials.
163+
164+
Add users through the admin UI (or `POST /admin/users`) **before** they sign in.
165+
On first successful SSO login DeepSQL links their Google identity to the
166+
existing account automatically; there is no separate linking step.
167+
168+
---
169+
170+
## Optional: disable password sign-in
171+
172+
Enabling SSO does **not** close the password path. `/auth/login` keeps working
173+
for every local account, so seed or demo credentials remain usable on an
174+
internet-facing install. To make SSO exclusive:
175+
176+
```bash
177+
SECURITY_PASSWORD_ENABLED=false
178+
```
179+
180+
The login page then hides the password form and offers only Google. The backend
181+
refuses password logins before any credential comparison and records a
182+
`PASSWORD_LOGIN_FAILURE` audit event with `reason=password_login_disabled`.
183+
184+
Defaults to `true`, so existing installs are unaffected.
185+
186+
> **Every user must be able to sign in with Google before you set this.** Anyone
187+
> who has only ever used a password is locked out until they use the Google
188+
> button once. Setting this while `SECURITY_GOOGLE_ENABLED=false` leaves nobody
189+
> able to sign in at all; DeepSQL logs an ERROR at startup if you do.
190+
191+
---
192+
193+
## Troubleshooting
194+
195+
| Symptom | Cause |
196+
| --- | --- |
197+
| No **Sign in with Google** button | `SECURITY_GOOGLE_ENABLED` is not `true`, or the backend was not recreated. Check `/api/setup/status`. |
198+
| `redirect_uri_mismatch` from Google | `SECURITY_GOOGLE_REDIRECT_URI` differs from the Cloud Console entry. Compare exactly — a trailing slash counts. |
199+
| `403 This Google Workspace domain is not allowed` | No enabled row in `google_workspace_domain`, or the account's `hd` claim does not match. Personal Gmail accounts have no `hd` and are always rejected. |
200+
| `403 This account is not provisioned in DeepSQL` | Authentication worked; the user does not exist in DeepSQL. Create them first. |
201+
| Lands on `localhost:3000` after sign-in | `APP_PUBLIC_URL` is unset. See [step 3](#3-set-your-public-url). |
202+
| Everyone locked out after `SECURITY_PASSWORD_ENABLED=false` | Set it back to `true`, recreate the backend, and confirm SSO works before disabling again. |
203+
204+
To see what actually happened, the `security_event` table records every attempt
205+
with its outcome and reason:
206+
207+
```sql
208+
SELECT created_at, event_type, outcome, email, event_metadata
209+
FROM security_event ORDER BY id DESC LIMIT 20;
210+
```

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ for a project overview and the fastest path to a running instance.
3939
- **[PERFORMANCE_UI_IMPLEMENTATION.md](./PERFORMANCE_UI_IMPLEMENTATION.md)** — Performance dashboard implementation
4040
- **[PERFORMANCE_UI_BUGFIXES.md](./PERFORMANCE_UI_BUGFIXES.md)** — Performance UI fixes and improvements
4141
- **[RBAC_USAGE_GUIDE.md](./RBAC_USAGE_GUIDE.md)** — Roles, permissions, and access control
42+
- **[GOOGLE_SSO_SETUP.md](./GOOGLE_SSO_SETUP.md)** — Google Workspace SSO and password-login control
4243

4344
## Operations
4445

0 commit comments

Comments
 (0)