Vetrix Docs

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 /userinfo call — then discarded.
  • Is not: a password replacement. OAuth-created accounts receive a randomly generated password hash; users recover password login via the existing /forgot-password flow.
  • 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
Google 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.

  1. Google Cloud Console → APIs & ServicesOAuth 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.
  2. CredentialsCreate CredentialsOAuth 2.0 Client ID.
    • Application type: Web application.
    • Name: Vetrix.
    • Authorized redirect URIs: the Google redirect URI from §3.
  3. Copy the generated client ID and client secret.
  4. 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) plus token_url_tls and jwks green.
    • Toggle EnabledSave changes.
  5. Open an incognito /login. You should see "Continue with Google".

4.2 GitHub

  1. 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.
  2. Generate a new client secret (shown once — copy it now).
  3. 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 /user endpoint returns a primary email but does not return an email_verified field. Vetrix therefore treats GitHub email as unverified unless you enable secondary verification via /user/emails. Practical consequence: do not enable the global oauth.allow_email_link toggle for GitHub-only deployments — the auto-link path requires email_verified=true.

4.3 GitLab

  1. GitLab → User Settings → Applications.
    • Name: Vetrix.
    • Redirect URI: the GitLab redirect URI from §3.
    • Scopes: openid, email, profile.
  2. Save and copy the resulting Application ID (the Client ID) and Secret.
  3. In Vetrix: Admin → Settings → OAuth Providers → GitLab.
    • Paste Client ID + Client Secret.
    • Leave scopes as default openid,email,profile.

4.4 Microsoft Entra

  1. 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.
  2. Certificates & secrets → Client secrets → New client secret. Copy the Value (not the Secret ID).
  3. API permissions — the default Microsoft Graph: User.Read is sufficient for the Part A email + profile scopes.
  4. In Vetrix: Admin → Settings → OAuth Providers → Microsoft.
    • Application (client) ID → Client ID.
    • Secret Value → Client Secret.
    • Single-tenant deployments: Vetrix uses the multi-tenant /common endpoint. Single-tenant tenant-ID pinning is not currently supported.

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_verified on their respective /userinfo and id_token payloads.
  • GitHub: does not assert email_verified via 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:

  1. Extend oauth2Presets in internal/auth/oauth2_providers.go with the preset's authorize / token / userinfo / JWKS / issuer URLs and default scopes.
  2. Extend oauth2ProviderNames in internal/admin/settings.go so the oauth.<new>.* keys get materialised at startup.
  3. Extend PROVIDER_ORDER in web/src/components/admin/OAuthProvidersGroup.tsx to include the new preset name.
  4. Write an operator guide section for this document.
  5. 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 /login and /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.