# 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//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:///_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/` 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/`** — 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//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//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:///_google-auth/callback ``` `` 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 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 ` | allow any verified `*@d` account (repeatable; replaces the stored list) | — | | `--allow-email ` | allow a specific address, e.g. an outside collaborator (repeatable) | — | | `--deny-email ` | 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 ` | how long a sign-in lasts (`24h`, `72h`, `30m`, …) | `24h` | | `--cookie-name ` | session cookie name | `_google_auth` | | `--port

` | 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:///_google-auth/` and sign out at `https:///_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 ``, 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 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 ```