Authentication
The platform uses Google OAuth for user authentication, managed through the gateway's auth module. Authentication is optional in development and mandatory in production.
Overview
When auth is enabled, the gateway protects non-public routes using a middleware that checks these credential types in order:
- API key --
X-API-Keyheader, compared via timing-safe equality againstSHIFT_API_KEY. - Delegated agent bearer token --
Authorization: Bearer sat_...header, resolved against the server-side delegated grant store. - Bearer token --
Authorization: Bearer <token>header, verified as a signed JWT. Used by the CLI after login. - Delegated agent browser cookie --
shift_agent_session, minted from a one-time bootstrap flow. - Session cookie --
shift_sessioncookie, also a signed JWT. Set during the browser OAuth flow.
If none of these credentials are present or valid, API requests receive a 401 JSON response. Browser requests (those accepting text/html) are redirected to /auth/login.
When Auth is Disabled
Authentication is controlled by three environment variables: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and SESSION_SECRET. In non-production environments, if any of these are missing, auth is disabled entirely and all routes are public.
In production (NODE_ENV=production), missing auth variables cause the gateway to throw on startup, preventing an insecure deployment.
Public Routes
The following prefixes are public, even when auth is enabled:
/healthz-- Gateway health check/auth/*-- Login page, OAuth flow, logout/s/*-- Stage shareable preview links/stage/*-- Stage web UI (public previews)/api/public/*-- Explicitly public API routes/api/v1/stage/*-- Stage preview and session routes by default/api/v1/hooks/*-- Webhook ingress
There are explicit protected exceptions under the Stage prefix:
/api/v1/stage/apps*/api/v1/stage/triggers*/api/v1/run/*
Auth Endpoints
GET /auth/login
Renders the login page. If the user already has a valid session cookie, redirects to /. Accepts an optional ?error= query parameter to display error messages.
GET /auth/google
Initiates the Google OAuth flow. Sets an OAuth state cookie for CSRF protection and redirects the user to Google's consent screen. Accepts an optional ?cli_port= parameter for CLI login flows.
GET /auth/callback
Handles the OAuth redirect from Google. Validates the state parameter against the cookie, exchanges the authorization code for user info via the auth provider, and either:
- Browser flow: Sets a session cookie and redirects to
/. - CLI flow: Creates a short-lived exchange code and redirects to
http://localhost:<cli_port>/callback?code=<exchange_code>.
POST /auth/exchange
Redeems a short-lived exchange code for a bearer token. Used by the CLI to complete the login flow. The exchange code is single-use and expires quickly.
POST /auth/logout
Revokes the current session token server-side and clears the session cookie. Redirects to /auth/login.
GET /api/v1/auth/me
Returns the authenticated user's profile (email, name, picture). Requires a valid session.
{
"success": true,
"data": {
"email": "user@the-shift.dev",
"name": "User Name",
"picture": "https://..."
}
}
Delegated Local Test Auth
The gateway also supports delegated local testing for coding agents and Playwright runs.
Delegated Grant Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /auth/agent/grants | Create a short-lived delegated test grant |
| GET | /auth/agent/grants | List active delegated grants for the current user |
| DELETE | /auth/agent/grants/:idOrLabel | Revoke a delegated grant |
| POST | /auth/agent/bootstrap | Exchange a delegated grant for a one-time bootstrap artifact |
| GET | /auth/agent/bootstrap?code=... | Redeem the one-time code into delegated browser cookies |
Delegated Cookies
| Cookie | Purpose |
|---|---|
shift_agent_session | Delegated browser session for platform routes |
shift_stage_agent | Delegated Stage end-user cookie for Stage app flows |
The browser bootstrap path is designed for local agent-driven tests. Raw delegated sat_... tokens should not be passed through browser query strings.
CLI Auth Flow
The CLI authenticates via a browser-based OAuth flow:
shift-cli loginstarts a temporary local HTTP server on a random port.- The CLI opens the browser to
https://app.the-shift.dev/auth/google?cli_port=<port>. - The user completes Google OAuth in the browser.
- The gateway's callback creates a short-lived exchange code and redirects to
http://localhost:<port>/callback?code=<code>. - The CLI's local server receives the code and calls
POST /auth/exchangeto redeem it for a bearer token. - The token is stored in
~/.shift/auth.json. - All subsequent CLI requests include the token as
Authorization: Bearer <token>.
Persistent OAuth via Passport
When a Stage app requests Google OAuth access, the platform stores refresh tokens in Passport for reuse across sessions. This avoids prompting the user for consent every time they open a new Stage app that requires the same scopes.
How It Works
- User authorizes a Stage app with Google OAuth scopes (e.g., Drive, Sheets).
- The gateway stores the refresh token in the
passport_secretstable, keyed by authorization SID. - The authorization record in
passport_authorizationstracks the user, provider, granted scopes, and status. - When the user opens another Stage app requesting the same or a subset of scopes, the gateway calls
findCoveringAuthorization()to check for an existing active authorization. - If a covering authorization exists, the OAuth consent screen is skipped entirely — the user is redirected directly to the app.
- If the refresh token becomes invalid (Google returns
invalid_grant), the authorization is marked as expired and the user is prompted to re-authorize.
Scope Merging
When a user grants new scopes to an existing authorization, the scopes are merged (union of old + new). This means authorizations accumulate scopes over time, reducing future consent prompts.
Storage
| Table | Purpose |
|---|---|
passport_authorizations | User-scoped authorization records with provider, scopes, and status |
passport_secrets | Refresh tokens keyed as passport/{authorizationSid}/refresh-token |
passport_audit | Lifecycle events (create, update, expire) for audit trail |
The passport_authorizations table has a composite index on (user, providerId) for efficient lookup of all authorizations a user holds with a given provider.
Secure Context Detection
Cookie security flags are set based on the request's transport protocol. The gateway checks x-forwarded-proto header first (for reverse proxy environments like Kubernetes), falling back to the request URL's protocol. Cookies are marked secure: true only when the detected protocol is HTTPS.
Domain Restriction
The SHIFT_ALLOWED_DOMAIN environment variable restricts which Google accounts can authenticate. Only users whose email domain matches the allowed domain (default: the-shift.dev) are permitted. The domain check is enforced by the auth provider during code exchange.
Rate Limiting
All /auth/* endpoints are rate-limited to 20 requests per minute per IP address. Rate limit state is stored in Convex (auth_rate_limits table) when using the Convex backend.
Token Revocation
When a user logs out, their token hash is stored in the auth_revoked_tokens table. The auth middleware checks this table on every request to ensure revoked tokens cannot be reused. Revocation entries are automatically cleaned up after the token's original expiration time.
Delegated test grants are tracked separately in auth_agent_grants, with one-time browser bootstrap records stored in auth_agent_bootstrap_codes.
Environment Variables
| Variable | Required | Purpose |
|---|---|---|
GOOGLE_CLIENT_ID | Production | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | Production | Google OAuth client secret |
SESSION_SECRET | Production | JWT signing secret (min 32 chars in production) |
SHIFT_API_KEY | No | Static API key for service-to-service auth |
SHIFT_ALLOWED_DOMAIN | No | Restrict login to a specific email domain (default: the-shift.dev) |
STAGE_GOOGLE_CLIENT_ID | No | Separate Google OAuth client for Stage end-user auth |
STAGE_GOOGLE_CLIENT_SECRET | No | Secret for the Stage Google OAuth client |