diff --git a/src/config.ts b/src/config.ts index 6e7a923..32581cb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,6 +20,8 @@ const configSchema = z.object({ googleClientId: z.string().default(""), googleClientSecret: z.string().default(""), googleCallbackUrl: z.string().default(""), + // Comma-separated allowlist of emails permitted to sign in via Google. Empty = allow any. + googleAllowedEmails: z.string().default(""), // Unipile unipileDsn: z.string().min(1), @@ -96,6 +98,7 @@ function loadConfig(): AppConfig { googleClientId: process.env.GOOGLE_CLIENT_ID, googleClientSecret: process.env.GOOGLE_CLIENT_SECRET, googleCallbackUrl: process.env.GOOGLE_CALLBACK_URL, + googleAllowedEmails: process.env.GOOGLE_ALLOWED_EMAILS, databaseUrl: process.env.DATABASE_URL, unipileDsn: process.env.UNIPILE_DSN, unipileAccessToken: process.env.UNIPILE_ACCESS_TOKEN, diff --git a/src/web/auth.ts b/src/web/auth.ts index 3f9f0ec..4672fbc 100644 --- a/src/web/auth.ts +++ b/src/web/auth.ts @@ -192,6 +192,16 @@ authRouter.get("/google/callback", async (req: Request, res: Response) => { const email = payload.email || ""; const displayName = payload.name || email; + // Enforce email allowlist (if configured) + const allowed = (appConfig.googleAllowedEmails || "") + .split(",") + .map((e) => e.trim().toLowerCase()) + .filter(Boolean); + if (allowed.length > 0 && !allowed.includes(email.toLowerCase())) { + console.warn("Google OAuth denied (not in allowlist):", email); + return res.redirect("/login?error=not_authorized"); + } + // Find user by google_id let found = await sql`SELECT * FROM users WHERE google_id = ${googleId}`;