313 lines
14 KiB
Markdown
313 lines
14 KiB
Markdown
# dokku-google-auth
|
|
|
|
Put Google OAuth SSO in front of any dokku app with one command. Configure
|
|
Google once, then:
|
|
|
|
```bash
|
|
dokku google-auth:enable my-app
|
|
```
|
|
|
|
Every request to `my-app` now requires a signed-in Google account from your
|
|
organization. The app itself never touches OAuth — it just reads the
|
|
signed-in user from request headers. Paths that should stay open (webhooks,
|
|
API-key-protected endpoints, health checks) can be excluded per app.
|
|
|
|
## How it works
|
|
|
|
```
|
|
┌──────────────────────── dokku host ────────────────────────┐
|
|
│ │
|
|
browser ── https ──▶ │ nginx (per-app vhost, unchanged dokku routing) │
|
|
│ │ │
|
|
│ ├─ auth_request GET /_google-auth/verify ──┐ │
|
|
│ │ 201? no: 302 to Google sign-in │ │
|
|
│ │ ▼ │
|
|
│ │ ┌────────────────────────┐ │
|
|
│ │ │ google-auth-proxy │ │
|
|
│ │ │ (one shared container, │ │
|
|
│ │ │ 127.0.0.1:2999) │ │
|
|
│ │ └────────────────────────┘ │
|
|
│ ▼ │
|
|
│ app container ◀── X-Forwarded-Email / X-Forwarded-User │
|
|
└────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
- **One shared Go service** (`google-auth-proxy`) runs in a single Docker
|
|
container on the host, bound to `127.0.0.1`. It holds the Google client
|
|
credentials and handles the entire OAuth dance.
|
|
- **Per-app nginx config** is injected through dokku's standard
|
|
`~dokku/<app>/nginx.conf.d/` include. It uses nginx's `auth_request`
|
|
module: every request triggers a fast subrequest to the auth service, which
|
|
checks an encrypted session cookie. No valid session → the browser is
|
|
redirected through Google sign-in and back.
|
|
- **Only one redirect URI is ever registered with Google** (the "auth
|
|
host"). When a user signs in to `app-a.example.com`, Google redirects to
|
|
`https://<auth-host>/_google-auth/callback`; the service verifies the
|
|
identity, then bounces the browser to `app-a` with a single-use, 60-second,
|
|
encrypted hand-off token, which becomes a session cookie scoped to
|
|
`app-a`'s own domain. Adding app #47 therefore requires **zero** changes in
|
|
the Google Console — apps don't even need to share a parent domain.
|
|
- Sessions, OAuth state, and hand-off tokens are all AES-256-GCM encrypted
|
|
and authenticated with a secret generated at configure time. Session
|
|
cookies are `Secure`, `HttpOnly`, `SameSite=Lax`, and pinned to the host
|
|
they were minted on.
|
|
|
|
### Headers your apps receive
|
|
|
|
On every authenticated request, nginx injects (and strips anything the
|
|
client tried to spoof):
|
|
|
|
| Header | Value |
|
|
|---|---|
|
|
| `X-Forwarded-Email` / `X-Auth-Request-Email` | signed-in Google email (lowercased) |
|
|
| `X-Forwarded-User` / `X-Auth-Request-User` | stable Google account id (`sub` claim) |
|
|
| `X-Auth-Request-Name` | display name |
|
|
|
|
On **excluded** paths these headers are set to the empty string, so your app
|
|
can trust that a non-empty `X-Forwarded-Email` always came from the plugin.
|
|
|
|
## How dokku plugins work (background)
|
|
|
|
A dokku plugin is just a git repo of executable shell scripts. Dokku clones
|
|
it into `/var/lib/dokku/plugins/available/<name>` and:
|
|
|
|
- **`plugin.toml`** — metadata (description, version).
|
|
- **`install`** — runs as root at `plugin:install` / `plugin:update` time.
|
|
This plugin uses it to create its data directory and `docker build` the Go
|
|
auth service image (multi-stage build, so the host only needs Docker, not
|
|
Go).
|
|
- **`commands`** + **`subcommands/<name>`** — dokku dispatches
|
|
`dokku google-auth:enable foo` to `subcommands/enable`, passing the full
|
|
command line. `commands` provides `dokku google-auth:help`. This is why the
|
|
plugin must be installed under the name `google-auth`.
|
|
- **Triggers** — executables named after lifecycle hooks that dokku (via
|
|
[plugn](https://github.com/dokku/plugn)) calls with arguments. This plugin
|
|
implements:
|
|
- `nginx-pre-reload` / `core-post-deploy` — regenerate the app's
|
|
`nginx.conf.d/google-auth.conf` on every deploy, so it always references
|
|
the app's current upstream (whose name changes if you remap ports).
|
|
- `post-delete`, `post-app-rename`, `post-app-clone` — keep plugin state in
|
|
sync with app lifecycle.
|
|
- **State** lives under `/var/lib/dokku/data/google-auth/` (global config +
|
|
one directory per enabled app). Secrets are `0600`.
|
|
|
|
The nginx integration relies on a stable, documented dokku feature: the
|
|
generated vhost for every app contains
|
|
`include /home/dokku/<app>/nginx.conf.d/*.conf;` inside the `server` block,
|
|
which is exactly where this plugin drops its `location` blocks.
|
|
|
|
## Installation
|
|
|
|
On the dokku host:
|
|
|
|
```bash
|
|
sudo dokku plugin:install https://github.com/<you>/dokku-google-auth.git --name google-auth
|
|
```
|
|
|
|
(The `--name google-auth` matters — subcommand dispatch is keyed off the
|
|
plugin directory name.)
|
|
|
|
Requirements: dokku with the default **nginx** proxy (not traefik/caddy),
|
|
Docker (always present on a dokku host), and nginx built with
|
|
`http_auth_request_module` (true for stock Debian/Ubuntu nginx).
|
|
|
|
### 1. Create the Google OAuth client (one time, ever)
|
|
|
|
1. Go to [Google Cloud Console → APIs & Services → Credentials](https://console.cloud.google.com/apis/credentials).
|
|
2. Configure the OAuth consent screen if you haven't: **Internal** user type
|
|
(recommended for a Workspace org — it automatically limits sign-in to your
|
|
organization and skips app verification).
|
|
3. **Create Credentials → OAuth client ID → Web application.**
|
|
4. Add exactly one **Authorized redirect URI**:
|
|
|
|
```
|
|
https://<auth-host>/_google-auth/callback
|
|
```
|
|
|
|
`<auth-host>` is any HTTPS domain that routes to an app you will enable
|
|
the plugin on. Pick your most permanent app's domain, or add a dedicated
|
|
domain (e.g. `auth.example.com`) to one of your apps with
|
|
`dokku domains:add <app> auth.example.com` (plus DNS + letsencrypt as
|
|
usual). All other apps piggyback on it.
|
|
|
|
5. Note the client ID and client secret.
|
|
|
|
### 2. Configure the plugin
|
|
|
|
`configure` is re-runnable: every flag it takes is persisted and can be
|
|
changed later by passing it again.
|
|
|
|
```bash
|
|
dokku google-auth:configure \
|
|
--client-id 1234567890-abc.apps.googleusercontent.com \
|
|
--client-secret GOCSPX-xxxxxxxxxxxx \
|
|
--auth-host auth.example.com \
|
|
--allow-domain signal.org
|
|
```
|
|
|
|
This starts the shared auth service container and prints the callback URL to
|
|
double-check against the Google Console.
|
|
|
|
Other flags (all optional, all persisted):
|
|
|
|
| Flag | Meaning | Default |
|
|
|---|---|---|
|
|
| `--allow-domain <d>` | allow any verified `*@d` account (repeatable; replaces the stored list) | — |
|
|
| `--allow-email <e>` | allow a specific address, e.g. an outside collaborator (repeatable) | — |
|
|
| `--deny-email <e>` | block a specific address even if the allow lists cover it (repeatable; replaces the stored list) | — |
|
|
| `--clear-deny-emails` | empty the deny list | — |
|
|
| `--session-ttl <dur>` | how long a sign-in lasts (`24h`, `72h`, `30m`, …) | `24h` |
|
|
| `--cookie-name <n>` | session cookie name | `_google_auth` |
|
|
| `--port <p>` | host port (127.0.0.1 only) for the auth service | `2999` |
|
|
| `--regenerate-cookie-secret` | rotate the session encryption key (signs everyone out) | — |
|
|
|
|
These flags **replace** the list they name, which suits initial setup. For
|
|
one-at-a-time changes afterwards, see the next section.
|
|
|
|
### 3. Decide who is allowed in
|
|
|
|
`--allow-domain` and `--allow-email` together form the **allowlist**, and at
|
|
least one entry is required. Any account matching neither is rejected — there
|
|
is no "allow everyone" mode. So to limit access to a specific list of people,
|
|
use only addresses and no domain:
|
|
|
|
```bash
|
|
dokku google-auth:allow greyson@signal.org alice@signal.org
|
|
```
|
|
|
|
The **deny list** is checked first and wins over both allow rules, which is
|
|
how you cut off one person without narrowing the whole domain:
|
|
|
|
```bash
|
|
dokku google-auth:allow signal.org # everyone at signal.org…
|
|
dokku google-auth:deny former@signal.org # …except this account
|
|
```
|
|
|
|
These four commands each change one entry at a time and restart the auth
|
|
service for you:
|
|
|
|
```bash
|
|
dokku google-auth:allow # show the allow list
|
|
dokku google-auth:allow signal.org # a domain (any verified account there)
|
|
dokku google-auth:allow guest@partner.com # one address
|
|
dokku google-auth:unallow guest@partner.com # remove either kind
|
|
|
|
dokku google-auth:deny # show the deny list
|
|
dokku google-auth:deny former@signal.org
|
|
dokku google-auth:undeny former@signal.org
|
|
```
|
|
|
|
Changes take effect on the affected user's **next request**: session cookies
|
|
are re-checked against the current lists rather than trusted until they
|
|
expire, so denying (or unallowing) someone with a live session ends it. To
|
|
sign out everyone at once instead, use
|
|
`configure --regenerate-cookie-secret`.
|
|
|
|
`unallow` refuses to remove the last allow entry, since an empty allowlist
|
|
locks everyone out of every enabled app. `dokku google-auth:report` shows
|
|
both lists as they currently stand.
|
|
|
|
### 4. Protect apps
|
|
|
|
```bash
|
|
dokku google-auth:enable my-app
|
|
dokku google-auth:enable other-app
|
|
...
|
|
```
|
|
|
|
That's it. Visit the app in a browser — you'll be bounced through Google and
|
|
back.
|
|
|
|
### 5. Exclude paths (optional, per app)
|
|
|
|
```bash
|
|
# Path prefix — everything under it is open:
|
|
dokku google-auth:exclude my-app /api/webhooks
|
|
|
|
# Regex (prefix with re:):
|
|
dokku google-auth:exclude my-app 're:^/(healthz|metrics)$'
|
|
|
|
# List / remove:
|
|
dokku google-auth:exclude my-app
|
|
dokku google-auth:unexclude my-app /api/webhooks
|
|
```
|
|
|
|
Excluded paths proxy straight to your app with identity headers blanked —
|
|
protect them yourself (API key, HMAC signature, etc.).
|
|
|
|
### Day-to-day commands
|
|
|
|
```bash
|
|
dokku google-auth:report # global + per-app status, incl. both access lists
|
|
dokku google-auth:report my-app # one app
|
|
dokku google-auth:allow alice@signal.org # let someone in
|
|
dokku google-auth:deny former@signal.org # cut someone off
|
|
dokku google-auth:disable my-app # turn SSO off for an app
|
|
dokku google-auth:logs -t # follow auth service logs (sign-ins, denials)
|
|
dokku google-auth:restart # restart the auth service
|
|
```
|
|
|
|
Users can check who they're signed in as at `https://<any-app>/_google-auth/`
|
|
and sign out at `https://<any-app>/_google-auth/logout`.
|
|
|
|
## Things worth knowing
|
|
|
|
- **HTTPS is required in practice.** Google refuses plain-HTTP redirect URIs
|
|
and the session cookie is marked `Secure`. Use dokku-letsencrypt as usual.
|
|
(`--insecure-allow-http` exists for local experiments only.)
|
|
- **The auth host must stay enabled.** Google's callback lands on
|
|
`<auth-host>`, so the app serving that domain must keep google-auth
|
|
enabled. `google-auth:disable` warns you if you disable that app.
|
|
- **Sessions are per-domain.** Signing in to `app-a` then visiting `app-b`
|
|
triggers another round-trip through Google, but it's silent (already
|
|
signed in) — the user just sees a quick redirect.
|
|
- **In-flight POSTs across an expired session** get redirected to sign-in and
|
|
are replayed as GETs — the POST body is lost. That's inherent to
|
|
redirect-based SSO; keep session TTLs comfortable (e.g. `72h`) if it bites.
|
|
- **Custom nginx location blocks** in your own `nginx.conf.d` files: plain
|
|
`location /foo` prefix blocks will be shadowed by this plugin's catch-all
|
|
regex location — use `location ^~ /foo` in your own snippets if you need
|
|
them to win.
|
|
- **Non-browser clients** (curl, fetch without `Accept: text/html`) get a
|
|
JSON `401` instead of a redirect chain.
|
|
- **Websockets** work — upgrade headers are forwarded exactly like dokku's
|
|
stock nginx template.
|
|
- **Plugin updates:** `sudo dokku plugin:update google-auth` rebuilds the
|
|
service image and restarts the container automatically.
|
|
|
|
## Uninstall
|
|
|
|
```bash
|
|
dokku google-auth:disable <each-app>
|
|
dokku google-auth:stop
|
|
sudo dokku plugin:uninstall google-auth
|
|
sudo rm -rf /var/lib/dokku/data/google-auth
|
|
docker image rm dokku-google-auth:latest
|
|
```
|
|
|
|
## Development
|
|
|
|
Tasks are defined in `mise.toml` ([mise](https://mise.jdx.dev) pins Go and
|
|
shellcheck):
|
|
|
|
```bash
|
|
mise install # install pinned toolchain
|
|
mise run test # Go unit tests (full OAuth flow against a fake Google)
|
|
mise run test-nginx-conf # generate nginx config + validate with real nginx in docker
|
|
mise run shellcheck # lint all plugin scripts
|
|
mise run check # everything CI would run
|
|
mise run docker-build # build the service image locally
|
|
```
|
|
|
|
Layout:
|
|
|
|
```
|
|
cmd/google-auth-proxy/ Go entrypoint
|
|
internal/authproxy/ OAuth flow, session crypto, HTTP handlers (+ tests)
|
|
functions shared bash for the plugin (config store, nginx generation)
|
|
subcommands/ dokku google-auth:* commands
|
|
install, nginx-pre-reload, core-post-deploy, post-* dokku lifecycle triggers
|
|
test/nginx-conf-test.sh bash integration test for the generated nginx config
|
|
Dockerfile multi-stage build → static binary in a scratch image
|
|
```
|