Raxx · internal docs

internal · gated

FreeScout Google OAuth SSO — setup and operations runbook

System: FreeScout oauthlogin module (Tagras OAuth & Social Login Module v1.0.26) on tickets.raxx.app Owner: Operator (Kristerpher) Issue: #961 Design refs: [ADR-0042](https://internal-docs.raxx.app/architecture/adr/0042-auth-unification-hybrid-model.html), auth-unification.md §6.2 Parent epic: #907 Last reviewed: 2026-05-12 UTC


Purpose

This runbook configures the FreeScout oauthlogin module so operators sign into tickets.raxx.app with their @raxx.app Google Workspace account. Local FreeScout passwords are retired after successful configuration.

Security invariants (from ADR-0042):


Vault paths

All credential names in this runbook are stored in Infisical at /MooseQuest/freescout/.

Secret name Description
GOOGLE_OAUTH_CLIENT_ID Client ID for the FreeScout-dedicated Google OAuth app
GOOGLE_OAUTH_CLIENT_SECRET Client Secret for the FreeScout-dedicated Google OAuth app
FREESCOUT_API_KEY FreeScout API key (pre-existing)
FREESCOUT_ADMIN_PASSWORD FreeScout admin password (pre-existing)

Never print the client secret to a terminal. Retrieve it only when pasting directly into the FreeScout UI or storing it in the vault.


Prerequisites

Before starting:

  1. The oauthlogin module is installed and activated at tickets.raxx.app/modules. (Confirmed installed per operator 2026-05-12 03:10 UTC.)
  2. The operator's FreeScout user account exists and is active. Google OAuth will only succeed if the @raxx.app Google email matches a pre-existing FreeScout user.
  3. Google Workspace admin access to the raxx.app organization (for creating an Internal OAuth app).
  4. A Google Cloud Console project is available under the raxx.app Workspace organization. If none exists, create one named raxx-internal-tools.

Step 1 — Create the Google OAuth 2.0 app in Cloud Console

Operator action. This step requires Workspace admin login to Google Cloud Console. The agent cannot execute this step autonomously.

This is a separate OAuth 2.0 app from the one used by CF Access in #960. Do not reuse that app.

1a. Navigate to the OAuth app creation screen

  1. Sign in to console.cloud.google.com with your @raxx.app Workspace admin account.
  2. In the top project selector, choose or create a project. Use raxx-internal-tools if it exists; otherwise create a new project with that name under the raxx.app organization.
  3. In the left nav, click APIs & ServicesCredentials.
  4. Click + Create Credentials at the top → OAuth client ID.

If the project has no OAuth consent screen, Google prompts you to configure one first.

  1. Click Configure Consent Screen.
  2. Under User Type, select Internal. This restricts the app to accounts in the raxx.app Workspace — no external Google accounts can authenticate.
  3. Click Create.
  4. Fill in the required fields: - App name: Raxx Support — FreeScout SSO - User support email: ops@raxx.app - Developer contact information: ops@raxx.app
  5. Under Scopes, add only the minimal required scopes: - openid - email - profile Do not add any Drive, Gmail, or Calendar scopes.
  6. Click Save and Continue through the remaining screens. No test users are needed (Internal apps skip that step).

1c. Create the OAuth Client ID

Back on the Credentials page:

  1. Click + Create CredentialsOAuth client ID.
  2. Under Application type, select Web application.
  3. In the Name field, enter: FreeScout oauthlogin module
  4. Under Authorized JavaScript origins, click + Add URI and enter: https://tickets.raxx.app
  5. Under Authorized redirect URIs, click + Add URI and enter: https://tickets.raxx.app/oauth/google/callback

    If the module uses a different callback path, check tickets.raxx.app/Modules/OAuthLogin/ for the route definition (see Discovering the callback path below).

  6. Click Create.

Google displays the Client ID and Client Secret in a dialog. Do not close this dialog until you have stored both in vault (Step 2).

Discovering the callback path

The Tagras oauthlogin module registers its callback route during php artisan module-install oauthlogin. The most reliable way to confirm the callback URL is to SSH to the instance and check the module's route file:

ssh -i /tmp/lightsail_us_east_1.pem -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null admin@54.146.13.200
grep -r "callback\|redirect" /var/www/html/freescout/Modules/OAuthLogin/Routes/ 2>/dev/null | head -20

Common paths used by this module family: - /oauth/google/callback (most common) - /login/oauth/callback - /auth/callback

If the SSH check reveals a different path, update the authorized redirect URI in the Cloud Console before proceeding.


Step 2 — Store credentials in Infisical vault

Operator action. Paste the Client ID and Client Secret from the Cloud Console dialog into vault. Never write them to a file or paste them into a terminal that logs output.

  1. Navigate to vault.raxx.app (CF Access gate applies).
  2. Open the MooseQuest project → environment production → path /freescout/.
  3. Create (or update) the following secrets: - Key: GOOGLE_OAUTH_CLIENT_ID — Value: paste the client ID from the Cloud Console dialog - Key: GOOGLE_OAUTH_CLIENT_SECRET — Value: paste the client secret from the Cloud Console dialog
  4. Click Save for each secret.
  5. Close the Cloud Console dialog (credentials are now safely in vault only).

Verification: from a terminal (without printing the secret value):

infisical secrets get GOOGLE_OAUTH_CLIENT_ID \
  --path /MooseQuest/freescout/ --plain | wc -c
# Should return a non-zero character count

Step 3 — Configure the oauthlogin module in FreeScout

Operator action. The FreeScout module configuration is UI-only. There is no API endpoint for module settings (confirmed per project_freescout_api_limits.md).

  1. Sign in to https://tickets.raxx.app (CF Access gate, then FreeScout credentials).
  2. Navigate to Manage (or the gear icon) → Modules → find the OAuth & Social Login module → click Settings (or the gear/configure icon on the module row).

If there is no Settings link, navigate directly to: https://tickets.raxx.app/oauth-login/settings or check Manage → OAuth Login in the left nav after the module is activated.

  1. In the module settings, configure the following fields:
Field Value
Provider Google
Client ID Retrieve from vault: GOOGLE_OAUTH_CLIENT_ID
Client Secret Retrieve from vault: GOOGLE_OAUTH_CLIENT_SECRET
Allowed / Hosted Domain raxx.app
Auto-create user on first login OFF / disabled
Default role for new users (leave blank — auto-create is OFF)

Critical: Auto-create must be OFF. ADR-0042 D5 forbids auto-provisioning through Google OAuth. An @raxx.app account that does not have a pre-existing FreeScout user must be rejected with an error, not silently provisioned.

Critical: The hosted domain restriction must be set to raxx.app. This enforces the hd claim check — only accounts with hd=raxx.app in their Google ID token are accepted. This is belt-and-suspenders alongside the CF Access email-domain policy.

  1. Click Save (or Update).

Retrieving credentials from vault for copy-paste

# Retrieve client ID (safe to print — not a secret):
infisical secrets get GOOGLE_OAUTH_CLIENT_ID \
  --path /MooseQuest/freescout/ --plain

# Retrieve client secret — DO NOT print to a shared terminal.
# Open a private terminal session, copy immediately, close:
infisical secrets get GOOGLE_OAUTH_CLIENT_SECRET \
  --path /MooseQuest/freescout/ --plain

Step 4 — Verify hd claim validation in module settings or source

The hd (hosted domain) claim in Google ID tokens asserts that the account belongs to a specific Google Workspace organization. Even though the OAuth consent screen is configured as Internal (which prevents external accounts from completing the auth flow), hd validation at the application layer is a belt-and-suspenders requirement (ADR-0042 D6).

Option A — UI confirmation

If the module settings page has a field labeled Hosted Domain, Allowed Domain, or Organization Domain, confirm it is set to raxx.app. This indicates the module validates the hd claim server-side before issuing a FreeScout session.

Option B — Source review (SSH)

SSH to the instance and inspect the module's Google provider:

ssh -i /tmp/lightsail_us_east_1.pem -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null admin@54.146.13.200

grep -r "hd\|hosted_domain\|hostedDomain" \
  /var/www/html/freescout/Modules/OAuthLogin/ 2>/dev/null

Expected: at least one occurrence of an hd parameter being passed to the Google OAuth authorization URL or being validated in the callback handler. If the source does not validate hd, document the gap as a comment on #961 and rely on the Internal consent screen as the sole restriction (noting this in the runbook).


Step 5 — Test the OAuth login flow

Operator action. Requires a browser session and a pre-existing FreeScout account mapped to a @raxx.app Google address.

  1. Open an incognito (private) browser window.
  2. Navigate to https://tickets.raxx.app/login.
  3. Complete the CF Access challenge (Google Workspace login through CF Access).
  4. On the FreeScout login page, look for the Sign in with Google button. Click it.
  5. Google's OAuth consent screen appears (if this is first use). It should show Raxx Support — FreeScout SSO as the app name. Authorize.
  6. You are redirected to https://tickets.raxx.app/oauth/google/callback (or the path confirmed in Step 1c).
  7. FreeScout looks up the Google email in its user table. Since the @raxx.app account exists as a pre-existing FreeScout user, the login succeeds.
  8. You arrive at the FreeScout inbox — the session is active.

Verification checks:

If login fails with "user not found" or similar:

The FreeScout user's email must exactly match the Google account email. Check the user record in FreeScout Admin → Users and confirm the email matches kris@moosequest.net (or whichever @raxx.app account is being tested).

Note: the operator account is kris@moosequest.net. If testing with a @raxx.app account, ensure that account has a matching FreeScout user. If testing only with kris@moosequest.net, ensure the Google account used in the OAuth flow is kris@moosequest.net.


Step 6 — Verify no token persistence in module source

SSH to the instance and confirm no Google-issued tokens (access token, refresh token, or raw ID token) are written to the FreeScout database or filesystem:

ssh -i /tmp/lightsail_us_east_1.pem -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null admin@54.146.13.200

# Check for token storage patterns in module source:
grep -rn "refresh_token\|access_token\|id_token" \
  /var/www/html/freescout/Modules/OAuthLogin/ 2>/dev/null

# Check for any new DB columns added by the module migration:
sudo mysql -u root freescout -e "DESCRIBE users;" | grep -i token
sudo mysql -u root freescout -e "SHOW TABLES LIKE '%oauth%';"

Expected results:

If the source persists tokens, document the finding as a security note on #961 and open a separate security-finding issue with severity:high + area:security.


Ongoing operations

Adding a new operator

  1. Create a FreeScout user account under Manage → Users → New User. Set the email to the operator's @raxx.app Google Workspace address.
  2. The operator signs in to tickets.raxx.app using the Sign in with Google button. FreeScout matches on email and issues a session.
  3. No separate FreeScout password is needed.

Revoking an operator's access

Two layers of revocation are required:

Layer Action Effect
CF Access (outer gate) Remove the operator from the CF Access policy (email allowlist or Workspace group) Operator can no longer reach tickets.raxx.app at all
FreeScout user (inner gate) Disable or delete the FreeScout user under Manage → Users Belt-and-suspenders: if CF Access is ever misconfigured, FreeScout rejects the login

Removing an operator from Google Workspace (suspending the account) prevents the OAuth flow from completing (the Workspace-internal OAuth app will not issue tokens to suspended accounts). However, always revoke both CF Access and the FreeScout user as belt-and-suspenders.

The FreeScout user record may be retained for audit purposes (ticket history attribution). Disabling is preferred over deleting unless a DSR erasure request requires deletion.

Updating the Google OAuth app (e.g., after credential rotation)

  1. In Google Cloud Console → APIs & Services → Credentials, open the FreeScout OAuth client.
  2. Generate a new client secret (the old one remains active until you delete it).
  3. Store the new secret in vault: GOOGLE_OAUTH_CLIENT_SECRET at /MooseQuest/freescout/.
  4. Update the FreeScout module settings (Step 3 above) with the new client secret.
  5. Delete the old client secret in Cloud Console.
  6. Test one login to confirm the new credential works.

After a FreeScout core update (git pull on the instance)

Module files may be overwritten by a FreeScout core update if the update includes a modules directory rewrite. After any git pull on the FreeScout instance:

cd /var/www/html/freescout
sudo -u www-data php artisan freescout:module-install oauthlogin
sudo -u www-data php artisan freescout:clear-cache

Verify module still shows Active in Manage → Modules. Re-run the login test (Step 5).


Troubleshooting

"Sign in with Google" button does not appear on FreeScout login page

Cause: Module is installed but not activated, or the module's routes are not registered.

ssh -i /tmp/lightsail_us_east_1.pem -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null admin@54.146.13.200
cd /var/www/html/freescout
sudo -u www-data php artisan module:list | grep oauth
sudo -u www-data php artisan freescout:module-install oauthlogin
sudo -u www-data php artisan freescout:clear-cache

"OAuth error: redirect_uri_mismatch"

Cause: The callback URL registered in Google Cloud Console does not match the URL the module is requesting.

  1. SSH to the instance and confirm the module's actual callback route (see Discovering the callback path).
  2. In Cloud Console → Credentials → the FreeScout OAuth client → update the Authorized redirect URI to match exactly.

"Login failed: user does not exist"

Cause: Auto-create is OFF (correct behavior). The Google account's email does not match any FreeScout user.

Fix: Create a FreeScout user with the exact matching email. Alternatively, if the login is for an existing user, check whether the email in FreeScout has a typo or trailing space.

"Login failed: unauthorized domain"

Cause: The Google account is not in the raxx.app Workspace (either wrong domain, or personal Google account).

This is expected and correct behavior. Only @raxx.app Workspace accounts (or kris@moosequest.net if that is the configured admin email) should be allowed. Do not widen the allowed domain.

OAuth flow completes but FreeScout session is not persisted (redirect loop)

Cause: FreeScout session cookie is blocked. This can happen if the APP_URL in .env does not match https://tickets.raxx.app exactly.

ssh -i /tmp/lightsail_us_east_1.pem -o StrictHostKeyChecking=no \
  -o UserKnownHostsFile=/dev/null admin@54.146.13.200
sudo grep '^APP_URL' /var/www/html/freescout/.env
# Must be: APP_URL=https://tickets.raxx.app

If wrong, correct it, then sudo -u www-data php artisan config:cache && sudo systemctl restart apache2.


Security notes