Skip to content

Order Ledger Google OAuth

Create the Gmail OAuth client for https://orders.infra.realemail.app and put that client into services/orders/.env.sops.yaml.

The app requests one scope, https://www.googleapis.com/auth/gmail.readonly. It does not send, modify, or delete mail. Leave the Google app in Testing. Do not publish it. A testing app can only be used by the addresses you add as test users, which is the right boundary for a single mailbox.

17TRACK and Telegram live in the same SOPS file. This procedure adds the Google client without replacing those keys. Ansible refuses to render the file while any of the four values is still REPLACE_ME_VIA_SOPS, so fill Telegram before you encrypt if you have those values now.

1. Pick or create a Google Cloud project

  1. Open Google Cloud console.
  2. In the project picker, create a project named order-ledger, or select one you already use for this app. Stay in that project for every step below.

2. Enable the Gmail API

  1. Open Gmail API.
  2. Confirm the project name at the top is order-ledger.
  3. Click Enable. If the button already says Manage, the API is on.

3. Register the OAuth app

  1. Open Google Auth Platform overview.
  2. If you see Get started, finish it:
  3. App name: Order Ledger
  4. User support email: the Gmail address this app will read
  5. Audience: External when that mailbox is a personal @gmail.com account. Choose Internal only when the mailbox is in a Google Workspace org you administer.
  6. Contact email: the same address
  7. Agree and continue
  8. Open Audience.
  9. Publishing status must stay Testing.
  10. Under Test users, click Add users and add the exact Gmail address whose mail will be read. Save.
  11. Open Data Access.
  12. Click Add or remove scopes.
  13. Find https://www.googleapis.com/auth/gmail.readonly (Gmail API, "View your email messages and settings").
  14. Select only that scope. Update, then Save.

If the console still shows the older APIs & Services > OAuth consent screen instead of Google Auth Platform, use that page for the same facts: External, Testing, your address as a test user, and the gmail.readonly scope.

4. Create the web client

  1. Open Clients.
  2. Click Create client.
  3. Application type: Web application. Do not choose Desktop, Android, or Chrome.
  4. Name: Order Ledger.
  5. Leave Authorized JavaScript origins empty.
  6. Under Authorized redirect URIs, click Add URI and enter this value with no trailing slash:

https://orders.infra.realemail.app/oauth/callback

  1. Click Create.

Optional, only if you still run the laptop copy: add a second redirect URI http://127.0.0.1:8766/oauth/callback. The homelab containers send the https://orders... URI. Google rejects the login when the URI in the request is not on this list character for character.

  1. On the client you just created, download the JSON. The file name looks like client_secret_....json.
  2. Open it in an editor and check the first key. It must be "web". A file whose first key is "installed" is the wrong client type. Delete that client and create a Web application again.

Keep that JSON file outside the git repo. Do not commit it, paste it into chat, or attach it to a ticket.

5. Put the client into the SOPS file

From the homelab repo root. CLIENT_JSON is the path to the file you downloaded.

umask 077
cd /path/to/homelab
test -f services/orders/.env.sops.yaml || cp services/orders/.env.sops.yaml.example services/orders/.env.sops.yaml

If services/orders/.env.sops.yaml is already encrypted (sops and ENC[ appear at the top), decrypt a working copy first. Skip this when the file is still plain YAML from the example:

tmp="$(mktemp)"
sops --decrypt --output-type yaml services/orders/.env.sops.yaml >"$tmp"
cp "$tmp" services/orders/.env.sops.yaml
rm -f "$tmp"

Set SEVENTEEN_TRACK_TOKEN, TELEGRAM_BOT_TOKEN, and TELEGRAM_ALLOWED_CHAT_ID in that plain file if they are not already real values. Then merge the downloaded client. This prints only the client id, not the secret:

uv run python - "$CLIENT_JSON" services/orders/.env.sops.yaml <<'PY'
import json
import sys
from pathlib import Path

import yaml

client_path, dest_path = sys.argv[1:]
raw = Path(dest_path).read_text(encoding="utf-8")
if "ENC[" in raw:
    raise SystemExit("destination is still encrypted; decrypt it before merging")
client = json.loads(Path(client_path).read_text(encoding="utf-8"))
web = client.get("web") or client.get("installed") or {}
if not isinstance(web, dict) or not web.get("client_id") or not web.get("client_secret"):
    raise SystemExit("JSON needs a web.client_id and web.client_secret")
if "installed" in client and "web" not in client:
    raise SystemExit("this is a desktop client; create a Web application instead")
data = yaml.safe_load(raw)
data["GOOGLE_CLIENT_JSON"] = client
Path(dest_path).write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
print("merged Google client", web["client_id"])
PY

Encrypt in place and remove the downloaded JSON from the repo tree if it was copied there:

sops --encrypt --in-place services/orders/.env.sops.yaml
chmod 600 services/orders/.env.sops.yaml

Before you commit, git diff services/orders/.env.sops.yaml must show ciphertext only. If you can read client_secret in the diff, stop and encrypt again. Do not push the plain file.

git add services/orders/.env.sops.yaml
git commit -m "Encrypt Order Ledger Google and bot secrets."

Commit that on a branch and open a pull request. The encrypted file is safe to merge. The age recipients in .sops.yaml already cover this path.

6. Render and connect

On infra-services, after that pull request is on main:

sudo env ANSIBLE_CONFIG=infra/ansible/ansible.cfg \
  ansible-playbook -i infra/ansible/inventory/generated.yml \
  infra/ansible/playbooks/site.yml --limit infra-services \
  --tags orders,orders-secrets \
  -e ansible_connection=local

Run that from /opt/homelab. ANSIBLE_CONFIG loads roles_path, and ansible_connection=local applies the play on this host. Without those, sudo looks for roles beside the playbook and then tries to SSH to root@192.168.6.17.

That writes /etc/homelab/orders/google_client.json as mode 0400 owned by uid 10001, and services/orders/compose.env for the API tokens. Then run the Komodo procedure deploy-orders if the containers are not already up.

In a browser on the LAN or Tailscale:

  1. Open https://orders.infra.realemail.app and sign in through Authentik.
  2. Click Connect Gmail.
  3. Choose the test-user address from step 3.
  4. Google will say the app is not verified. Click Advanced, then Go to Order Ledger (unsafe). That warning is expected while the app stays in Testing.
  5. Allow the read-only Gmail scope. You should land back on the dashboard with Gmail connected.
  6. Click Sync Gmail. The first four-month scan can take a few minutes.

When Google refuses the login

What you see What to change
redirect_uri_mismatch The console URI is not exactly https://orders.infra.realemail.app/oauth/callback. No trailing slash, no http.
access_denied or the account cannot continue That Google account is not on the Testing user list, or the app was published and then blocked.
App is blocked / verification required The consent screen was published. Set it back to Testing.
Connected, then "no refresh token" Open Google account permissions, remove Order Ledger, and click Connect Gmail again.
Dashboard never reaches Google Authentik did not let you in. /oauth/start is behind Authentik on purpose. The callback path is the one that skips it.