Skip to content

Authentication & MCP Auth

VibeXP is provider-agnostic and brings its own OAuth 2.1 Authorization Server in-house, so you can self-host MCP authentication with any supported login provider — without a third-party auth service and without the MCP surface changing per provider.

The MCP authorization spec separates two roles. VibeXP plays both, so an MCP client only ever talks to your VibeXP instance:

RoleWhoWhat it does
Resource ServerVibeXP (/mcp/v1/*)Validates the bearer token (audience-bound JWT), serves RFC 9728 Protected Resource Metadata
Authorization ServerVibeXP (/oauth2/*)Dynamic Client Registration, PKCE, consent, token issuance, JWKS — RFC 8414 metadata
Identity ProviderYour choice (Google / GitHub / OIDC)Authenticates the human at login time only — invisible to MCP

VibeXP mints short-lived JWTs (audience = your MCP_RESOURCE_URI), signs them with DB-stored keys it rotates, and publishes its own JWKS. Compliance is therefore constant across providers.

Set AUTH_PROVIDER to a single provider. Each enabled provider needs its own credentials. A misconfigured provider is logged and skipped (never fatal), so you can roll out one at a time.

Terminal window
# One of: google, github, oidc
AUTH_PROVIDER=google

To enable several providers at once, set the provider list in a mounted config.yaml (it takes precedence over AUTH_PROVIDER and is not settable via env vars):

auth:
providers: ["google", "github"]
Terminal window
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_REDIRECT_URI=https://<your-app-host>/api/v1/auth/callback
Terminal window
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
GITHUB_REDIRECT_URI=https://<your-app-host>/api/v1/auth/callback

Generic OIDC (Keycloak, Zitadel, Auth0, Okta, WorkOS, …)

Section titled “Generic OIDC (Keycloak, Zitadel, Auth0, Okta, WorkOS, …)”

Any standards-compliant OpenID Connect provider works through the generic adapter:

Terminal window
OIDC_ISSUER_URL=https://id.example.com/ # used for OIDC discovery
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...
OIDC_REDIRECT_URI=https://<your-app-host>/api/v1/auth/callback

Register <your-app-host>/api/v1/auth/callback as an allowed redirect URI in the provider’s console.

Terminal window
ENCRYPTION_KEY=<32 raw bytes> # AES-256 at-rest key: app data + OAuth-AS signing-key encryption
SESSION_ENCRYPTION_KEY=<64 hex chars> # encrypts the web app session cookie (vx_session)

The web app keeps a server-side cookie session; the embedded Authorization Server is for MCP / programmatic OAuth clients. Both share the same provider config and user provisioning. The AS’s JWT signing keys are generated, stored encrypted in the database, rotated, and served via JWKS — there are no key files to manage, and it is multi-instance safe.

Enable the embedded Authorization Server (MCP auth)

Section titled “Enable the embedded Authorization Server (MCP auth)”

The AS is opt-in: it stays off (routes unmounted) until you set its public issuer URL.

Terminal window
# Public base URL of THIS VibeXP instance (enables the AS when set)
OAUTH_AS_ISSUER_URL=https://<your-app-host>
# Token audience (RFC 8707) — your MCP endpoint. Required to enable the MCP endpoint.
MCP_RESOURCE_URI=https://<your-app-host>/mcp/v1/common

The token lifetimes are tunable under auth.oauth_as.* in a mounted config.yaml — they are not env-wired in the published image (sane defaults shown):

auth:
oauth_as:
access_token_ttl: "15m"
refresh_token_ttl: "720h"
auth_code_ttl: "10m"
key_rotation_interval: "720h"
cleanup_interval: "1h"

When OAUTH_AS_ISSUER_URL is set, MCP_OAUTH_ISSUER defaults to it; setting a divergent MCP_OAUTH_ISSUER fails fast at startup (a token’s issuer and the resource server’s expected issuer must agree).

MCP clients (Claude Code, Cursor, …) need no API key: they perform Dynamic Client Registration, PKCE, and consent against /oauth2/* automatically. See MCP Server Integration for the client side.

FRONTEND_BASE_URL must point at your app’s public origin — it drives the consent redirect and post-login landing:

Terminal window
FRONTEND_BASE_URL=https://<your-app-host>

For local-only evaluation you can skip external providers entirely:

Terminal window
# Leave AUTH_PROVIDER unset — no external provider needed.
FRONTEND_BASE_URL=http://localhost:8080 # localhost ⇒ development mode

The dev-login bypass (/api/v1/auth/dev/login) is on by default (auth.dev_login_enabled: true in the shipped config) but is honored only in local development — a non-localhost FRONTEND_BASE_URL makes it inert.

In local development the embedded AS auto-derives sane defaults (issuer = the local base URL, resource URI = <issuer>/mcp/v1/common) and HTTPS enforcement is skipped, so a fresh checkout connects an MCP client with zero auth edits. Production never derives these and never bypasses HTTPS.

Restrict sign-in by email domain and/or exact address. Both are env-wired in the published image:

Terminal window
AUTH_ALLOWED_DOMAINS=example.com
AUTH_ALLOWED_EMAILS=alice@example.com

A user is allowed if either list matches (comma-separate multiple values). Both empty means anyone the provider authenticates can sign in. While the allowlist is active, sign-in also requires a provider-verified email, so unverified accounts are denied.

To set it in a mounted config.yaml instead:

auth:
access_allowlist:
domains: ["example.com"]
emails: ["alice@example.com"]

Instance admins get the admin portal (/api/v1/admin). List them via INSTANCE_ADMIN_EMAILS (comma-separated), or auth.instance_admins in a mounted config.yaml:

Terminal window
INSTANCE_ADMIN_EMAILS=admin@example.com

Empty (the default) leaves the feature dormant.

  • No WorkOS (or any external Authorization Server) account.
  • No JWT key files — keys live in the database and rotate automatically.
  • No CORS or reverse-proxy routing config for MCP — the AS and the SPA are same-origin in the single combined image.