Skip to content

Commit 562b117

Browse files
committed
docs: add Google Workspace SSO setup guide
The four SECURITY_GOOGLE_* variables were undocumented — absent from .env.example and unmentioned in the README — and two required steps are not discoverable from configuration alone: - The domain allowlist is a database table (google_workspace_domain), not an environment variable. Without a row, SSO rejects every sign-in while appearing fully configured. - SSO does not provision accounts. A valid Workspace user who was never added to DeepSQL gets "403 This account is not provisioned in DeepSQL", which reads as a bug rather than the deliberate authorization boundary it is. Also documents two traps found while setting this up on a real deployment: - 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, app.base-url only the CLI device flow. Setting the wrong one leaves sign-in succeeding but landing the user on localhost:3000/dashboard. - The Audience setting 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. Adds docs/GOOGLE_SSO_SETUP.md, the variables to .env.example (including SECURITY_PASSWORD_ENABLED), and index entries in README.md and docs/README.md.
1 parent 7b8ea65 commit 562b117

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
@@ -439,6 +439,7 @@ caching · nginx.
439439
- [`docs/README.md`](docs/README.md) — documentation index
440440
- [`AGENTS.md`](AGENTS.md) — codebase map
441441
- [`mcp/README.md`](mcp/README.md) — CLI and MCP server
442+
- [`docs/GOOGLE_SSO_SETUP.md`](docs/GOOGLE_SSO_SETUP.md) — Google Workspace SSO, and disabling password sign-in
442443
- [`SECURITY.md`](SECURITY.md) — reporting a vulnerability
443444

444445
## 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)