Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ ALLOW_UNAUTHENTICATED=
# silently serving a login-gated app to anonymous visitors.
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
# Canonical Google OAuth names. Configure these in Vercel Production.
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Temporary compatibility fallback only. Remove after production verification.
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
# Production-only escape hatch for a deliberately public deployment. Accepts
Expand Down
9 changes: 6 additions & 3 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# and non-public /api/*). Outside production it just leaves routes ungated.
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-here
GOOGLE_OAUTH_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=your-google-client-secret
# GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET are also supported.
# Canonical Google OAuth names. Configure these in Vercel Production.
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Temporary compatibility fallback only. Remove after production verification.
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
# Opt out of gating in production on purpose (1/true/yes/on). Ignored when
# NEXTAUTH_SECRET is set.
AUTH_ALLOW_UNAUTHENTICATED=
Expand Down
12 changes: 7 additions & 5 deletions apps/web/src/lib/__tests__/auth-config-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ describe('auth configuration source safety', () => {
expect(source).not.toContain('<Image');
});

it('accepts both project-specific and common Google OAuth env names', () => {
it('prefers canonical Google OAuth env names while retaining legacy fallbacks', () => {
const source = readSource('lib/auth.ts');
expect(source).toContain('GOOGLE_OAUTH_CLIENT_ID');
expect(source).toContain('GOOGLE_CLIENT_ID');
expect(source).toContain('GOOGLE_OAUTH_CLIENT_SECRET');
expect(source).toContain('GOOGLE_CLIENT_SECRET');
expect(source.indexOf('process.env.GOOGLE_CLIENT_ID')).toBeLessThan(
source.indexOf('process.env.GOOGLE_OAUTH_CLIENT_ID'),
);
expect(source.indexOf('process.env.GOOGLE_CLIENT_SECRET')).toBeLessThan(
source.indexOf('process.env.GOOGLE_OAUTH_CLIENT_SECRET'),
);
});

it('keeps the root route as a sell Home instead of redirecting to the dashboard', () => {
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import GoogleProvider from 'next-auth/providers/google';

const allowedDomain = process.env.AUTH_ALLOWED_EMAIL_DOMAIN?.trim().toLowerCase();
const googleClientId = (
process.env.GOOGLE_OAUTH_CLIENT_ID ||
process.env.GOOGLE_CLIENT_ID ||
process.env.GOOGLE_OAUTH_CLIENT_ID ||
''
).trim();
const googleClientSecret = (
process.env.GOOGLE_OAUTH_CLIENT_SECRET ||
process.env.GOOGLE_CLIENT_SECRET ||
process.env.GOOGLE_OAUTH_CLIENT_SECRET ||
''
).trim();

/**
* NextAuth configuration (Google OAuth by default).
*
* Required env to activate login-gating: NEXTAUTH_SECRET, NEXTAUTH_URL,
* GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET.
* Also accepts NextAuth's common GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET names.
* GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET.
* Temporarily accepts legacy GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET.
* Optional: AUTH_ALLOWED_EMAIL_DOMAIN restricts sign-in to a single domain
* (e.g. `yourcompany.com` → only *@yourcompany.com).
*
Expand All @@ -31,7 +31,7 @@ function buildProviders(): NextAuthOptions['providers'] {
if (!googleClientId || !googleClientSecret) {
if (process.env.NODE_ENV === 'production') {
console.error(
'[auth] Google OAuth client id/secret missing — set GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET or GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET.',
'[auth] Google OAuth client id/secret missing — set GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET; legacy GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET remain supported temporarily.',
);
}
}
Expand Down
12 changes: 12 additions & 0 deletions docs/deployment/VERCEL_PRODUCTION_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ shipped code.
- `npm run build:web`
- `npm --prefix apps/web audit --omit=dev`

## Google OAuth Production Migration

- Configure `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in Vercel Production.
These are the canonical names used by the app.
- `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET` remain temporary
fallbacks only. Do not remove them until a production deployment has completed
a real Google sign-in and callback using the canonical names.
- In Google Cloud, authorize exactly
`https://uvai.io/api/auth/callback/google` as the production redirect URI.
- Never record credential values in repository files, deployment logs, issue
comments, or build artifacts.

## Incident Response

- Severity owner: project owner on the Vercel team.
Expand Down