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):
- Google OAuth on FreeScout is an auth path only, not a registration path. An operator must exist in FreeScout's local user table before Google OAuth can authenticate them. A valid
@raxx.appaccount that is not pre-provisioned is rejected with 403. Auto-create is OFF. hdclaim validation is mandatory: the module must be configured to accept only accounts whose Google ID token carrieshd=raxx.app.- CF Access outer gate remains active. OAuth login flows run behind CF Access — the operator must already hold a valid CF Access session before the FreeScout OAuth redirect is initiated.
- No Google access token, refresh token, or ID token is stored by FreeScout. The module exchanges the code for an ID token, reads the email claim, and issues a FreeScout PHP session. Nothing Google-issued is persisted to the FreeScout DB. (Verify this in module source — see Step 6.)
- The Google OAuth app for FreeScout is a separate Cloud Console app from the CF Access IDP app provisioned in #960. Sharing OAuth apps between CF Access and an application-layer relying party creates audit confusion and violates least-privilege on the OAuth app scope.
- PKCE is required on the authorization flow per ADR-0042 D7. Confirm the module supports PKCE or note the gap for a future module version.
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:
- The
oauthloginmodule is installed and activated attickets.raxx.app/modules. (Confirmed installed per operator 2026-05-12 03:10 UTC.) - The operator's FreeScout user account exists and is active. Google OAuth will only succeed if the
@raxx.appGoogle email matches a pre-existing FreeScout user. - Google Workspace admin access to the
raxx.apporganization (for creating an Internal OAuth app). - A Google Cloud Console project is available under the
raxx.appWorkspace organization. If none exists, create one namedraxx-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
- Sign in to console.cloud.google.com with your
@raxx.appWorkspace admin account. - In the top project selector, choose or create a project. Use
raxx-internal-toolsif it exists; otherwise create a new project with that name under theraxx.apporganization. - In the left nav, click APIs & Services → Credentials.
- Click + Create Credentials at the top → OAuth client ID.
1b. Configure the OAuth consent screen (if prompted)
If the project has no OAuth consent screen, Google prompts you to configure one first.
- Click Configure Consent Screen.
- Under User Type, select Internal. This restricts the app to accounts in the
raxx.appWorkspace — no external Google accounts can authenticate. - Click Create.
- Fill in the required fields:
- App name:
Raxx Support — FreeScout SSO- User support email:ops@raxx.app- Developer contact information:ops@raxx.app - Under Scopes, add only the minimal required scopes:
-
openid-email-profileDo not add any Drive, Gmail, or Calendar scopes. - 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:
- Click + Create Credentials → OAuth client ID.
- Under Application type, select Web application.
- In the Name field, enter:
FreeScout oauthlogin module - Under Authorized JavaScript origins, click + Add URI and enter:
https://tickets.raxx.app - Under Authorized redirect URIs, click + Add URI and enter:
https://tickets.raxx.app/oauth/google/callbackIf the module uses a different callback path, check
tickets.raxx.app/Modules/OAuthLogin/for the route definition (see Discovering the callback path below). - 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.
- Navigate to vault.raxx.app (CF Access gate applies).
- Open the
MooseQuestproject → environmentproduction→ path/freescout/. - 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 - Click Save for each secret.
- 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).
- Sign in to
https://tickets.raxx.app(CF Access gate, then FreeScout credentials). - 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.
- In the module settings, configure the following fields:
| Field | Value |
|---|---|
| Provider | |
| 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.appaccount 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 thehdclaim check — only accounts withhd=raxx.appin their Google ID token are accepted. This is belt-and-suspenders alongside the CF Access email-domain policy.
- 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.appGoogle address.
- Open an incognito (private) browser window.
- Navigate to
https://tickets.raxx.app/login. - Complete the CF Access challenge (Google Workspace login through CF Access).
- On the FreeScout login page, look for the Sign in with Google button. Click it.
- Google's OAuth consent screen appears (if this is first use). It should show Raxx Support — FreeScout SSO as the app name. Authorize.
- You are redirected to
https://tickets.raxx.app/oauth/google/callback(or the path confirmed in Step 1c). - FreeScout looks up the Google email in its user table. Since the
@raxx.appaccount exists as a pre-existing FreeScout user, the login succeeds. - You arrive at the FreeScout inbox — the session is active.
Verification checks:
- The FreeScout session is issued. The inbox loads. No error page.
- Try the same flow with a Google account that is NOT a pre-existing FreeScout user. The login should fail (error page, not auto-create a new FreeScout user). This verifies auto-create is OFF.
- Confirm the CF Access outer gate is still active: in a second incognito window, navigate to
https://tickets.raxx.appwithout a CF Access cookie. You should be redirected to the CF Access login page, not directly to FreeScout.
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.appaccount, ensure that account has a matching FreeScout user. If testing only withkris@moosequest.net, ensure the Google account used in the OAuth flow iskris@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:
- No
refresh_tokenoraccess_tokencolumns appear in theuserstable. - Any
oauth_*tables, if they exist, should store only the Googlesubidentifier (a stable, opaque string) — not tokens. id_tokenreferences in source should be read-only (parsing the email claim) and not persisted.
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
- Create a FreeScout user account under Manage → Users → New User. Set the email to the operator's
@raxx.appGoogle Workspace address. - The operator signs in to
tickets.raxx.appusing the Sign in with Google button. FreeScout matches on email and issues a session. - 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)
- In Google Cloud Console → APIs & Services → Credentials, open the FreeScout OAuth client.
- Generate a new client secret (the old one remains active until you delete it).
- Store the new secret in vault:
GOOGLE_OAUTH_CLIENT_SECRETat/MooseQuest/freescout/. - Update the FreeScout module settings (Step 3 above) with the new client secret.
- Delete the old client secret in Cloud Console.
- 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.
- SSH to the instance and confirm the module's actual callback route (see Discovering the callback path).
- 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
- Separate OAuth app per surface. The FreeScout OAuth app is not shared with the CF Access IDP app from #960. If credentials for one are compromised, the other is not affected.
- Internal consent screen. The OAuth consent screen is configured as Internal (Workspace-only). External Google accounts cannot see or complete the authorization flow.
hdclaim. The module's allowed domain setting corresponds to thehdparameter in the OAuth authorization URL. When set, Google's authorization endpoint enforces Workspace account selection. The callback handler must also validate thehdclaim in the returned ID token (belt-and-suspenders, per ADR-0042 D6).- No stored tokens. Google-issued tokens are transient. FreeScout receives an authorization code, exchanges it for an ID token (short-lived), reads the email, and issues a FreeScout PHP session. The ID token is not stored. Verified in Step 6.
- CF Access outer gate remains active. Even if the FreeScout OAuth login is misconfigured or compromised, CF Access is a separate network-layer gate. Both must be bypassed for unauthorized access. The CF Access policy for
tickets.raxx.appuses the Google IDP with@raxx.appemail enforcement. - Audit trail. FreeScout logs logins to its access log. SSH:
sudo tail -f /var/log/apache2/freescout-access.logfor real-time monitoring.