OAuth2 External Providers (Part A)
Vetrix supports "Sign in with Google / GitHub / GitLab / Microsoft" as an additive authentication path. Existing username/password + TOTP flows are never replaced. This document is the operator guide: how to enable a provider, where to register the redirect URI, what to expect when it works, and what to check when it doesn't.
1. What Part A is (and isn't)
- Is: delegated authentication. An external identity provider vouches that a user is who they say they are; Vetrix then issues its own session.
- Is not: delegated authorization. Vetrix does not pull repos from
GitHub, calendars from Google, or organisations from GitLab. Provider
access tokens are used once — for the
/userinfocall — then discarded. - Is not: a password replacement. OAuth-created accounts receive a
randomly generated password hash; users recover password login via the
existing
/forgot-passwordflow. - Is not: SAML, LDAP, or federated directory sync. Those are tracked
separately under
internal/auth/saml.go/ldap.go.
2. Prerequisites
Before you enable any provider:
| Requirement | Where | Why |
|---|---|---|
SECRET_ENC_KEY env var set (32-byte hex) |
Server process | The oauth.<provider>.client_secret is AES-256-GCM sealed with this key. Missing ⇒ the admin UI's "Save" succeeds but login fails on decrypt. |
server.external_url accurate |
app.toml |
The computed default redirect URL derives from this value. An operator-supplied redirect URL overrides it per provider. |
| HTTPS end-to-end in production | Reverse proxy | State cookies are Secure; OAuth standards mandate TLS on redirect URIs. |
| Log rotation configured | /var/log/vetrix/ |
Every audit action (user.oauth_login, user.oauth_login_failed, user.oauth_link*, user.oauth_unlink, user.oauth_account_created) lands here. |
3. The redirect URI
Every provider needs the exact same redirect URI registered in its console:
${server.external_url}/api/v1/auth/oauth2/{provider}/callback
Examples assuming server.external_url = https://www.gitvetrix.com:
| Provider | Redirect URI |
|---|---|
https://www.gitvetrix.com/api/v1/auth/oauth2/google/callback |
|
| GitHub | https://www.gitvetrix.com/api/v1/auth/oauth2/github/callback |
| GitLab | https://www.gitvetrix.com/api/v1/auth/oauth2/gitlab/callback |
| Microsoft Entra | https://www.gitvetrix.com/api/v1/auth/oauth2/microsoft/callback |
Overriding oauth.<provider>.redirect_url in /admin/settings uses the
exact value you type instead of the computed default. Mismatches against
the value registered at the provider will reject the callback with
redirect_uri_mismatch.
4. Per-provider setup
4.1 Google
Target time: under 15 minutes for a fresh Google Cloud project.
- Google Cloud Console → APIs & Services → OAuth consent screen.
- User type: "Internal" if this is a Google Workspace deployment, otherwise "External".
- Fill in the required application name, support email, and developer contact information.
- Credentials → Create Credentials → OAuth 2.0 Client ID.
- Application type: Web application.
- Name: Vetrix.
- Authorized redirect URIs: the Google redirect URI from §3.
- Copy the generated client ID and client secret.
- In Vetrix: Admin → Settings → OAuth Providers → Google.
- Paste Client ID.
- Paste Client Secret (field input; it is AES-GCM sealed on save).
- Scopes: leave as default
openid,email,profile. - Click Test configuration — you should see three green ticks
(
client_id,client_secret,redirect_url) plustoken_url_tlsandjwksgreen. - Toggle Enabled → Save changes.
- Open an incognito
/login. You should see "Continue with Google".
4.2 GitHub
- GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.
- Application name: Vetrix.
- Homepage URL: your
server.external_url. - Authorization callback URL: the GitHub redirect URI from §3.
- Generate a new client secret (shown once — copy it now).
- In Vetrix: Admin → Settings → OAuth Providers → GitHub.
- Paste Client ID + Client Secret.
- Scopes: leave as default
read:user,user:email. - Caveat —
email_verified. GitHub's/userendpoint returns a primary email but does not return anemail_verifiedfield. Vetrix therefore treats GitHub email as unverified unless you enable secondary verification via/user/emails. Practical consequence: do not enable the globaloauth.allow_email_linktoggle for GitHub-only deployments — the auto-link path requiresemail_verified=true.
4.3 GitLab
- GitLab → User Settings → Applications.
- Name: Vetrix.
- Redirect URI: the GitLab redirect URI from §3.
- Scopes:
openid,email,profile.
- Save and copy the resulting Application ID (the Client ID) and Secret.
- In Vetrix: Admin → Settings → OAuth Providers → GitLab.
- Paste Client ID + Client Secret.
- Leave scopes as default
openid,email,profile.
4.4 Microsoft Entra
- Azure Portal → Microsoft Entra ID → App registrations → New registration.
- Name: Vetrix.
- Supported account types: pick Accounts in any organizational directory (multitenant) and personal Microsoft accounts unless your tenant requires single-tenant.
- Redirect URI: Web, value = Microsoft redirect URI from §3.
- Certificates & secrets → Client secrets → New client secret. Copy the Value (not the Secret ID).
- API permissions — the default Microsoft Graph: User.Read is
sufficient for the Part A
email+profilescopes. - In Vetrix: Admin → Settings → OAuth Providers → Microsoft.
- Application (client) ID → Client ID.
- Secret Value → Client Secret.
- Single-tenant deployments: Vetrix uses the multi-tenant
/commonendpoint. Single-tenant tenant-ID pinning is not currently supported.
5. The oauth.allow_email_link toggle
By default, an OAuth login whose email matches an existing local account
is refused with HTTP 409 — the authenticating user has to log in with
password first and link from /settings/security.
Turning the toggle on auto-links on successful OAuth when the provider
asserts email_verified=true. The security trade-off: if a provider ever
marks an attacker-controlled email as verified, an attacker could take
over the matching local account. Current posture:
- Google, GitLab, Microsoft: all assert
email_verifiedon their respective/userinfoandid_tokenpayloads. - GitHub: does not assert
email_verifiedvia the default endpoint. Leaving the toggle off is the safe default for GitHub-only deployments.
6. Interaction with 2FA
If auth.require_2fa=true or the individual user has TOTP enrolled, the
OAuth callback does not set session cookies directly. Instead it
issues a short-lived vetrix_mfa_pending cookie and 302s to
/login/mfa?flow=oauth. The same TOTP verification endpoint that finishes
a password login finishes an OAuth login — there is one code path, not two.
This means an attacker in possession of a valid Google session can still not sign into Vetrix on behalf of a TOTP-enrolled user.
7. Auditing
Every OAuth flow emits structured audit entries in audit_log:
| Action | When |
|---|---|
user.oauth_login |
Successful login via an existing link |
user.oauth_login_failed |
Any non-success (state mismatch, id_token invalid, email in use, account disabled, …). details.reason carries the tag. |
user.oauth_account_created |
New user created via finish-signup |
user.oauth_link |
Authenticated user added a new link |
user.oauth_link_auto |
Auto-link path under oauth.allow_email_link=true |
user.oauth_unlink |
Authenticated user removed a link |
Tokens, nonces, PKCE verifiers, and client secrets are never written to the audit log.
Query examples:
-- Failed OAuth attempts in the last hour, grouped by reason.
SELECT details->>'reason' AS reason, COUNT(*)
FROM audit_log
WHERE action = 'user.oauth_login_failed'
AND created_at > NOW() - INTERVAL '1 hour'
GROUP BY 1
ORDER BY 2 DESC;
-- A specific user's OAuth history.
SELECT created_at, action, details
FROM audit_log
WHERE actor_id = $1 AND action LIKE 'user.oauth_%'
ORDER BY created_at DESC;
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Provider consent screen shows redirect_uri_mismatch |
The URI registered at the provider does not match the computed default or the override in /admin/settings. |
Register the exact URI from §3, or reconcile the override. |
Callback returns 400 state invalid |
The sealed cookie could not be decrypted. Typical cause: SECRET_ENC_KEY changed or is unset; or the cookie was truncated by a reverse proxy. |
Ensure SECRET_ENC_KEY is stable and set before the server starts; ensure the proxy does not strip cookies > 4 KB. |
Callback returns 502 provider exchange failed |
The token endpoint was unreachable or returned non-200. | Confirm network egress; test curl -I https://{provider-token-url} from the Vetrix host. |
Callback returns 401 id_token invalid (OIDC) |
Signature failed, issuer/audience mismatch, expired, or nonce mismatch. | Run Test configuration in /admin/settings → check jwks and token_url_tls; verify the registered client_id matches the aud you expect. |
"Continue with Google" missing on /login |
oauth.google.enabled=false, oauth.google.client_id blank, or the admin has not yet saved the config. |
/admin/settings → OAuth Providers → Google → Enabled after credentials. |
| After enabling, an old signup ticket URL returns "link used or expired" | Tickets are single-use, 10-min TTL. | Start the flow again from /login. |
9. Extending the preset registry
Adding a brand-new provider is a code change, not runtime configuration. This is intentional — the preset registry's pinned URLs are the primary SSRF / open-redirect mitigation.
To add a new provider:
- Extend
oauth2Presetsininternal/auth/oauth2_providers.gowith the preset's authorize / token / userinfo / JWKS / issuer URLs and default scopes. - Extend
oauth2ProviderNamesininternal/admin/settings.goso theoauth.<new>.*keys get materialised at startup. - Extend
PROVIDER_ORDERinweb/src/components/admin/OAuthProvidersGroup.tsxto include the new preset name. - Write an operator guide section for this document.
- PR and merge; an admin can then configure the new provider via
/admin/settings.
10. Kill switch
Flipping oauth.<provider>.enabled to false at any time immediately:
- hides the provider button on
/loginand/register, - returns 404 on
GET /api/v1/auth/oauth2/{provider}/start, and - returns 404 on
GET /api/v1/auth/oauth2/{provider}/callback.
Existing linked users with a password can still log in unchanged. Existing linked users without a usable password should regain access via the forgot-password flow before you flip the switch off.