OAuth Providers
This guide walks through registering OAuth 2.0 / OpenID Connect apps for the four providers Orkestra supports out of the box: Google, Apple, GitHub, and Discord. Email/password login works without any of these — OAuth is opt-in.
:::info Where credentials live
As of the auth-module ConfigService refactor (PR-C), OAuth client IDs, secrets, and redirect URLs are runtime configuration owned by the auth module. The recommended workflow:
- (Optional) Seed env vars on a fresh install — the auth module reads
OAUTH_<PROVIDER>_*on first boot and writes them intomodule_configsin MongoDB. - Manage them from then on at
/admin/modules/authin the operator console. Secrets are AES-256-GCM encrypted at rest.
Env-var names follow the convention OAUTH_<PROVIDER>_<FIELD> (e.g. OAUTH_GOOGLE_CLIENT_ID). Older docs that show GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET without the OAUTH_ prefix are stale.
See backend/internal/core/auth/AGENTS.md for the canonical field-by-field schema.
:::
Prerequisites
- Access to the respective provider developer portals (Google Cloud Console, Apple Developer, GitHub, Discord)
- Orkestra backend running and reachable at your operator and client hosts. Set
ALLOW_LOCALHOST_REDIRECTS=trueindocker/.envfor development; do not ship that to production.
Google OAuth setup
Step 1: access Google Cloud Console
- Go to Google Cloud Console.
- Sign in with your Google account.
- Create a new project or select an existing one.
Step 2: enable required APIs
- Navigate to APIs & Services → Library.
- Search for and enable:
- Google+ API
- Google Identity Toolkit API
Step 3: configure OAuth consent screen
- Go to APIs & Services → OAuth consent screen.
- Choose External user type (unless for internal organization).
- Fill in the required information:
- App name: Orkestra
- User support email: your support email
- App logo: upload your logo (optional)
- Application home page:
https://yourdomain.com - Authorized domains: add your domain(s)
- Developer contact information: your email
- Add scopes:
openid,email,profile. - Save and continue.
Step 4: create OAuth 2.0 credentials
- Navigate to APIs & Services → Credentials.
- Click Create Credentials → OAuth client ID.
- Select Web application as the application type.
- Configure:
- Name: Orkestra Web Client
- Authorized JavaScript origins:
https://console.orkestra.com(operator, prod)https://api.orkestra.com(client, prod)http://localhost:8080(dev, operator)http://client.localhost:8081(dev, client)
- Authorized redirect URI (one callback, on the operator backend, not the frontend):
https://console.orkestra.com/v1/auth/oauth/google/callbackhttp://localhost:3000/v1/auth/oauth/google/callback(dev, operator) The signed OAuthstateselects the operator or client auth service after this callback, so client-tier flows do not need a client-host callback URI.
- Click Create.
- Save the Client ID and Client Secret — paste them into
/admin/modules/authunder the Google group (or setOAUTH_GOOGLE_CLIENT_ID/OAUTH_GOOGLE_CLIENT_SECRETas seed env vars on first boot).
Step 5: mobile app configuration (if needed)
- Create another OAuth client ID.
- Select iOS or Android as the application type.
- For iOS: enter your Bundle ID.
- For Android: enter your Package name and your SHA-1 certificate fingerprint.
- Save the configuration.
Apple Sign In setup
Step 1: access Apple Developer account
- Go to Apple Developer.
- Sign in with your Apple ID.
- Ensure you have an active Developer Program membership.
Step 2: register App ID
- Navigate to Certificates, Identifiers & Profiles.
- Click Identifiers → + button.
- Select App IDs and click Continue.
- Select App and click Continue.
- Fill in:
- Description: Orkestra
- Bundle ID: Choose Explicit and enter
com.yourdomain.orkestra
- Under Capabilities, enable Sign In with Apple.
- Click Continue and Register.
Step 3: create Service ID (for Web)
- In Identifiers, click + button.
- Select Services IDs and click Continue.
- Fill in:
- Description: Orkestra Web Service
- Identifier:
com.yourdomain.orkestra.web
- Click Continue and Register.
- Click on the created Service ID to configure.
- Enable Sign In with Apple.
- Click Configure next to Sign In with Apple.
- Set:
- Primary App ID: select your App ID from Step 2.
- Domains and Subdomains: add the operator callback host (for example,
console.orkestra.com). Apple does not acceptlocalhost; use ngrok for local development. - Return URL (one callback on the operator backend):
https://console.orkestra.com/v1/auth/oauth/apple/callbackThe signed OAuthstatedispatches the completed flow to the client tier when that is where it started; do not register a client-host callback.
- Click Next, Done, and Continue.
- Click Save.
Step 4: create private key
- Navigate to Keys → + button.
- Enter a Key Name: Orkestra Auth Key.
- Enable Sign In with Apple.
- Click Continue and Register.
- Download the private key (
.p8file).- IMPORTANT: save this file securely — you can't download it again.
- Note the Key ID shown on the page.
Step 5: gather required information
You'll need:
- Team ID: found in your Apple Developer account (top right corner).
- Service ID: the identifier created in Step 3 (e.g.
com.yourdomain.orkestra.web). - Key ID: from Step 4.
- Private Key: the
.p8file downloaded in Step 4.
Apple private-key incident response
Treat a suspected Apple .p8 private-key exposure as an incident. In order:
- Identify every affected environment and the Apple configuration each uses.
- Restrict access to, and sanitize where retention policy permits, local and OTLP log records that could contain the key or related credential material. Preserve only the incident evidence required by policy.
- In Apple Developer, rotate the key and invalidate the exposed key.
- Update the encrypted
authmodule configuration with the replacement Apple key and its matching key ID. - Do not restart a service unless the live configuration behavior requires it; the application does not rotate an external Apple key for you.
- Force reauthentication for affected users and monitor OAuth failures while the replacement is in use.
GitHub OAuth setup
- Go to GitHub Developer Settings → OAuth Apps.
- Click New OAuth App and fill in:
- Application name: Orkestra
- Homepage URL:
https://console.orkestra.com(or your operator host) - Authorization callback URL:
https://console.orkestra.com/v1/auth/oauth/github/callback
- Register the app, then click Generate a new client secret.
- Save the Client ID and Client Secret — paste them into
/admin/modules/authunder the GitHub group (or useOAUTH_GITHUB_CLIENT_ID/OAUTH_GITHUB_CLIENT_SECRETas seed env vars). - Register the single operator-host callback above. The signed OAuth
statedispatches a completed client-tier flow to the client auth service, so a second client-host callback or provider app is not required for the audience split.
Discord OAuth setup
- Go to the Discord Developer Portal and create a New Application.
- Open OAuth2 → General. Add the single operator-host redirect:
https://console.orkestra.com/v1/auth/oauth/discord/callbackhttp://localhost:3000/v1/auth/oauth/discord/callback(dev) The signed OAuthstatedispatches client-tier flows after the callback; do not add a client-host callback.
- Click Reset Secret to generate a client secret.
- Save the Client ID and Client Secret — paste them into
/admin/modules/authunder the Discord group (orOAUTH_DISCORD_CLIENT_ID/OAUTH_DISCORD_CLIENT_SECRET). - Required scopes Orkestra requests:
identify email.
Environment configuration
Backend (seed env vars, optional)
These env vars only seed module_configs: they are read on first boot and on any later boot at which the key is still absent — SeedFromModules back-fills every absent schema key from its EnvVar on every boot — and they are ignored as soon as a value is stored, because the stored value then wins. So manage values from /admin/modules/auth (or PATCH /v1/admin/modules/auth). Secrets are AES-256-GCM encrypted at rest by ConfigService.
# Google OAuth
OAUTH_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
OAUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
OAUTH_GOOGLE_REDIRECT_URL=https://console.orkestra.com/v1/auth/oauth/google/callback
# Apple Sign In
OAUTH_APPLE_TEAM_ID=your-team-id
OAUTH_APPLE_CLIENT_ID=com.yourdomain.orkestra.web # Apple Service ID
OAUTH_APPLE_KEY_ID=your-key-id
OAUTH_APPLE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\n... # inline PEM (preferred)
OAUTH_APPLE_PRIVATE_KEY_PATH=/app/keys/AuthKey_XXXXXX.p8 # OR file fallback
OAUTH_APPLE_REDIRECT_URL=https://console.orkestra.com/v1/auth/oauth/apple/callback
# GitHub
OAUTH_GITHUB_CLIENT_ID=your-github-client-id
OAUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
OAUTH_GITHUB_REDIRECT_URL=https://console.orkestra.com/v1/auth/oauth/github/callback
# Discord
OAUTH_DISCORD_CLIENT_ID=your-discord-client-id
OAUTH_DISCORD_CLIENT_SECRET=your-discord-client-secret
OAUTH_DISCORD_REDIRECT_URL=https://console.orkestra.com/v1/auth/oauth/discord/callback
# JWT — Orkestra uses RS256 (asymmetric), NOT a shared HMAC secret.
# `make init` generates these for you under docker/keys/ — see init.sh.
JWT_PRIVATE_KEY_PATH=/app/keys/jwt-private.pem
JWT_PUBLIC_KEY_PATH=/app/keys/jwt-public.pem
Frontend
The browser never talks to OAuth providers directly — it starts at the tier-specific backend endpoint POST /v1/auth/{tier}/oauth/login, which returns the provider authorization URL with state, scopes, and the configured single operator-host callback. So the frontend does not need provider client IDs.
# frontend-admin (operator console) — .env.production
VITE_API_URL=https://console.orkestra.com
# frontend-client (Tier-2 client SPA) — .env.production
VITE_API_BASE=https://api.orkestra.com
Mobile
For the Flutter mobile app, update:
iOS — ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.YOUR_GOOGLE_IOS_CLIENT_ID</string>
</array>
</dict>
</array>
Android — add the Google services configuration file to android/app/google-services.json.
Testing OAuth integration
Local development
-
Ensure your backend is running and reachable as both
http://localhost:3000(operator — an unmatchedHostfalls through to the operator mux in development) andhttp://client.localhost:3000(client) — the host mux dispatches onHost. The client API shares the client SPA's hostname because the client-tier cookies areSameSite=Laxand sibling*.localhostlabels are different sites; add entries to/etc/hostsif your resolver needs them:127.0.0.1 client.localhost -
Start the frontends — operator on
http://localhost:8080, client SPA onhttp://client.localhost:8081.http://localhost:8080is the shipped operator entry point: it matches the defaultVITE_API_URL=http://localhost:3000, so the console's origin and its API are same-site, and it is the hostname the console's dev server allows out of the box.Two host rules bind the operator OAuth walk-through in step 3, and the shipped
localhostlayout satisfies both. Theorkestra_oauth_statecookie is host-only andSameSite=Lax(oauth_state_binding.go:48-58), so the host that answers the login POST must be the host that answers the callback — both arelocalhost:3000here, which is why the dev callback registered above ishttp://localhost:3000/v1/auth/oauth/google/callback— the value the auth module's Redirect URL field for Google must also hold, since that field, not any compiled fallback, is what the backend sends the IdP. And the console's origin must stay same-site withVITE_API_URLor the refresh cookie is lost the same way;scripts/env-validate.shrefuses a mixed pairing (spec §8 follow-up #13). Putting the console onconsole.localhostend to end works too — originhttp://console.localhost:8080,VITE_API_URL=http://console.localhost:3000,console.localhostinVITE_ADMIN_ALLOWED_HOSTS— but then the registered callback has to move tohttp://console.localhost:3000/...with it, and the module's Redirect URL at/admin/modules/authhas to say so (OAUTH_GOOGLE_REDIRECT_URLonly seeds that field while it is still unset — any boot back-fills an absent key from the variable — and is ignored once a value is stored). -
From the operator login page, start Google OAuth through
POST /v1/auth/operator/oauth/login. The browser follows the returned authorization URL to Google, then returns tohttp://localhost:3000/v1/auth/oauth/google/callback, the single operator-host callback. -
Repeat from the client SPA (
http://client.localhost:8081/login) — its provider buttons callPOST /v1/auth/client/oauth/login. The signed state carriestier=client, so the same operator-host callback performs only the IdP half and relays the outcome tohttp://client.localhost:3000/v1/auth/client/oauth/complete?relay=…on the client API host, which verifies the browser binding against the state cookie it set at start, issues theaud=clientsession and the client-tier refresh cookie, and lands onhttp://client.localhost:8081/auth/callback, where the SPA adopts the cookie throughPOST /v1/auth/client/refresh-cookie.
Production
- Deploy your application to your domain.
- Ensure HTTPS is properly configured.
- Test both authentication flows (operator + client).
- Verify tokens are properly generated and stored.
Going SSO-only
Once a provider works, a surface can stop accepting email/password
altogether. The two switches live under Login & Sessions at
/admin/modules/auth: passwordLoginEnabledAdmin for the operator
console and passwordLoginEnabledClient for the client app. They are
independent — take one surface SSO-only and leave the other alone.
Do it in this order.
- Configure the provider and verify it end to end on the surface you are about to close, following the setup guide above. A provider counts only when its toggle for that surface is on and every field the flow needs is filled in: client ID, redirect URL and client secret — for Apple, also team ID, key ID and a private key (inline or a readable, non-empty key file).
- Sign in through it at least once, with an account you will still need afterwards. The identity provider must return the address as verified; an unverified one can neither sign up nor attach to an existing account.
- Leave "Auto-link OAuth provider to existing email account"
(
oauthAutoLinkByEmail) ON. This is what lets accounts created with a password reach their account through the provider afterwards. It is part of the guard below, not a nicety. - Turn the surface's
passwordLoginEnabled…switch off and save. The change is live immediately — no restart, no restart banner.
What the validator refuses. A save that would leave a surface with no
way to sign in is rejected with 422 auth.login_method_lockout, and
nothing is written — neither the config values nor the module's enabled
state. That covers an ordinary save, a save into a named environment, and
activating an environment whose own configuration is invalid, so you
cannot walk into a lockout sideways. In practice you will meet it when
you try to disable the last usable provider for a closed surface, blank
one of its required fields, remove its secret, or turn auto-link off.
A field that is not a literal true/false is refused separately,
naming that field.
Two more behaviours are worth knowing before you flip:
- Existing sessions are not revoked. Whoever is signed in stays signed in. Flipping the switch governs the next authentication.
- Password-related recovery closes with it. Signup, forgot-password
and the operator's send password reset stop working for that surface
(the last one answers
409);reset-password,accept-invite,verify-emailandchange-passwordstay open so a recovery already in flight can finish.
Walk the checks in the auth module reference before and after the flip: password login refused with the stable code and the form gone, OAuth login working, an admin password reset refused, and unlinking the sole usable identity refused.
The break-glass drill
If the console is SSO-only and OAuth then breaks — a rotated secret, a lost key, an outage at the identity provider — there is a boot-time escape hatch that restores operator password login only:
# docker/.env
AUTH_OPERATOR_PASSWORD_LOGIN_BREAK_GLASS=true
- Set it, then restart the backend through the sanctioned stack
lifecycle (
./orkestra.sh). - Sign in on the console's labelled emergency form. MFA still applies, and so does every RBAC and step-up gate on what you do next.
- Repair the provider configuration at
/admin/modules/auth. - Unset the variable and restart again.
It is deliberately narrow: it rescues POST /v1/auth/operator/login and
the MFA continuation of a login it allowed — nothing else. Registration,
forgot/reset password, password reconfirmation, the OAuth-unlink guard
and the whole client surface stay closed, and the persisted policy is
never rewritten. While the variable is set the backend logs a warning on
every boot, and every rescued sign-in is audited with the audience,
the user id, the session id and the source IP — never a credential. Treat
a lingering warning in the logs as an unfinished incident.
Rehearse it on staging before you need it: set the variable, restart, confirm the emergency form appears and an operator can sign in while signup and forgot-password stay absent and client login stays refused, then unset it, restart, and confirm the form and the warning are gone.
Security best practices
- Never commit secrets to version control.
- Use environment variables (or, better, ConfigService) for all sensitive configuration.
- Implement HTTPS for production deployments.
- Rotate keys periodically.
- Implement rate limiting on authentication endpoints (built in —
RATE_LIMIT_REQUESTS_PER_MINUTE/RATE_LIMIT_BURST). - Log authentication attempts for security monitoring (built in — see
complianceaddon). - Use secure session management with JWT tokens (built in — RS256, refresh-token rotation).
- Implement refresh token rotation (built in — see
backend/internal/core/auth/).
Troubleshooting
Common Google OAuth issues
Error: redirect_uri_mismatch
- Ensure the redirect URI in your request matches exactly with configured URIs.
- Check for trailing slashes and protocol (
httpvshttps).
Error: invalid_client
- Verify Client ID and Client Secret are correct.
- Check if the OAuth consent screen is properly configured.
Common Apple Sign In issues
Error: invalid_grant
- Ensure the authorization code is used only once.
- Verify the redirect URI matches exactly.
Error: invalid_client
- Check Team ID, Service ID, and Key ID are correct.
- Verify the private key file is accessible and valid.
JWT token issues
- Ensure the private key file has correct permissions.
- Verify the key hasn't expired.
- Check the algorithm matches (ES256 for Apple).
Support
- Google OAuth: Google Identity Platform documentation
- Apple Sign In: Sign in with Apple documentation
- Orkestra issues: open one on the monorepo
Pre-production checklist
- Google OAuth credentials created and configured for the intended audiences
- Apple Sign In credentials created and configured (operator callback host; ngrok used for local)
- GitHub OAuth app created with the operator-host callback
- Discord OAuth credentials created and configured
- Credentials stored in
/admin/modules/auth(ConfigService), not in plain.envin prod - Each provider's redirect URI is registered on the operator host (
console.orkestra.comin these examples) - RS256 keys (
jwt-private.pem/jwt-public.pem) generated andJWT_PRIVATE_KEY_PATH/JWT_PUBLIC_KEY_PATHset - HTTPS enabled on production
- OAuth tested in development environment (both operator and client tiers)
- OAuth tested in production environment
- Security best practices implemented
- Monitoring and logging configured
- Documentation shared with team