feat(security): Add development mode bypass for Turnstile token validation
- Implement development mode flag to skip Turnstile token validation - Update registration and login API routes to handle optional token in dev mode - Enhance development workflow by simplifying security checks
- commit
- 6e4a0f0264251c775b003a1c92180e44f990338e
- author
- Kit · · 2025-02-18 16:08 UTC
- parent
- 272f39f
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
3 files · +24 -10
M src/lib/utils/turnstile.server.ts +7 -0
| @@ -2,6 +2,7 @@ import { TURNSTILE_SECRET_KEY } from '$env/static/private'; | |||
| 2 | 2 | import { createLogger } from './logger.server'; | |
| 3 | 3 | ||
| 4 | 4 | const log = createLogger('turnstile'); | |
| 5 | + | const isDev = process.env.NODE_ENV === 'development'; | |
| 5 | 6 | ||
| 6 | 7 | interface TurnstileVerifyResponse { | |
| 7 | 8 | "success": boolean; | |
| @@ -13,6 +14,12 @@ interface TurnstileVerifyResponse { | |||
| 13 | 14 | } | |
| 14 | 15 | ||
| 15 | 16 | export async function validateTurnstileToken(token: string, remoteip?: string): Promise<boolean> { | |
| 17 | + | // Always return true in development mode | |
| 18 | + | if (isDev) { | |
| 19 | + | log.debug('Development mode: bypassing Turnstile validation'); | |
| 20 | + | return true; | |
| 21 | + | } | |
| 22 | + | ||
| 16 | 23 | try { | |
| 17 | 24 | const formData = new FormData(); | |
| 18 | 25 | formData.append('secret', TURNSTILE_SECRET_KEY); | |
| @@ -2,6 +2,7 @@ import { TURNSTILE_SECRET_KEY } from '$env/static/private'; | |||||
| 2 | import { createLogger } from './logger.server'; | 2 | import { createLogger } from './logger.server'; | ||
| 3 | 3 | ||||
| 4 | const log = createLogger('turnstile'); | 4 | const log = createLogger('turnstile'); | ||
| 5 | + | const isDev = process.env.NODE_ENV === 'development'; | |||
| 5 | 6 | ||||
| 6 | interface TurnstileVerifyResponse { | 7 | interface TurnstileVerifyResponse { | ||
| 7 | "success": boolean; | 8 | "success": boolean; | ||
| @@ -13,6 +14,12 @@ interface TurnstileVerifyResponse { | |||||
| 13 | } | 14 | } | ||
| 14 | 15 | ||||
| 15 | export async function validateTurnstileToken(token: string, remoteip?: string): Promise<boolean> { | 16 | export async function validateTurnstileToken(token: string, remoteip?: string): Promise<boolean> { | ||
| 17 | + | // Always return true in development mode | |||
| 18 | + | if (isDev) { | |||
| 19 | + | log.debug('Development mode: bypassing Turnstile validation'); | |||
| 20 | + | return true; | |||
| 21 | + | } | |||
| 22 | + | ||||
| 16 | try { | 23 | try { | ||
| 17 | const formData = new FormData(); | 24 | const formData = new FormData(); | ||
| 18 | formData.append('secret', TURNSTILE_SECRET_KEY); | 25 | formData.append('secret', TURNSTILE_SECRET_KEY); | ||
M src/routes/api/register/+server.ts +7 -5
| @@ -8,6 +8,7 @@ import { createLogger } from '$lib/utils/logger.server'; | |||
| 8 | 8 | import { eq } from 'drizzle-orm/sql'; | |
| 9 | 9 | ||
| 10 | 10 | const log = createLogger('register-server'); | |
| 11 | + | const isDev = process.env.NODE_ENV === 'development'; | |
| 11 | 12 | ||
| 12 | 13 | // In-memory map to track failed captcha attempts per IP | |
| 13 | 14 | const captchaAttempts = new Map<string, { count: number, lastAttempt: number }>(); | |
| @@ -50,22 +51,23 @@ export const POST: RequestHandler = async ({ request }) => { | |||
| 50 | 51 | suPassword: string, | |
| 51 | 52 | suConfirmPassword: string, | |
| 52 | 53 | captchaAnswer: string, | |
| 53 | - | turnstileToken: string | |
| 54 | + | turnstileToken?: string | |
| 54 | 55 | }; | |
| 55 | 56 | ||
| 56 | - | if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || !turnstileToken) { | |
| 57 | + | if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || (!isDev && !turnstileToken)) { | |
| 57 | 58 | log.warn('Missing required fields', { | |
| 58 | 59 | hasUsername: Boolean(suUsername), | |
| 59 | 60 | hasPassword: Boolean(suPassword), | |
| 60 | 61 | hasConfirmPassword: Boolean(suConfirmPassword), | |
| 61 | 62 | hasCaptcha: Boolean(captchaAnswer), | |
| 62 | - | hasTurnstileToken: Boolean(turnstileToken) | |
| 63 | + | hasTurnstileToken: Boolean(turnstileToken), | |
| 64 | + | isDev | |
| 63 | 65 | }); | |
| 64 | 66 | return new Response(JSON.stringify({ error: 'All fields are required' } as RegisterResponseError), { status: 400 }); | |
| 65 | 67 | } | |
| 66 | 68 | ||
| 67 | - | // Validate Turnstile token | |
| 68 | - | const isValidTurnstile = await validateTurnstileToken(turnstileToken, ip); | |
| 69 | + | // Validate Turnstile token (will be automatically bypassed in dev mode) | |
| 70 | + | const isValidTurnstile = await validateTurnstileToken(turnstileToken || '', ip); | |
| 69 | 71 | if (!isValidTurnstile) { | |
| 70 | 72 | log.warn('Invalid Turnstile token', { maskedIp }); | |
| 71 | 73 | return new Response(JSON.stringify({ error: 'Security check failed. Please try again.' } as RegisterResponseError), { status: 400 }); | |
| @@ -8,6 +8,7 @@ import { createLogger } from '$lib/utils/logger.server'; | |||||
| 8 | import { eq } from 'drizzle-orm/sql'; | 8 | import { eq } from 'drizzle-orm/sql'; | ||
| 9 | 9 | ||||
| 10 | const log = createLogger('register-server'); | 10 | const log = createLogger('register-server'); | ||
| 11 | + | const isDev = process.env.NODE_ENV === 'development'; | |||
| 11 | 12 | ||||
| 12 | // In-memory map to track failed captcha attempts per IP | 13 | // In-memory map to track failed captcha attempts per IP | ||
| 13 | const captchaAttempts = new Map<string, { count: number, lastAttempt: number }>(); | 14 | const captchaAttempts = new Map<string, { count: number, lastAttempt: number }>(); | ||
| @@ -50,22 +51,23 @@ export const POST: RequestHandler = async ({ request }) => { | |||||
| 50 | suPassword: string, | 51 | suPassword: string, | ||
| 51 | suConfirmPassword: string, | 52 | suConfirmPassword: string, | ||
| 52 | captchaAnswer: string, | 53 | captchaAnswer: string, | ||
| 53 | - | turnstileToken: string | 54 | + | turnstileToken?: string |
| 54 | }; | 55 | }; | ||
| 55 | 56 | ||||
| 56 | - | if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || !turnstileToken) { | 57 | + | if (!suUsername || !suPassword || !suConfirmPassword || !captchaAnswer || (!isDev && !turnstileToken)) { |
| 57 | log.warn('Missing required fields', { | 58 | log.warn('Missing required fields', { | ||
| 58 | hasUsername: Boolean(suUsername), | 59 | hasUsername: Boolean(suUsername), | ||
| 59 | hasPassword: Boolean(suPassword), | 60 | hasPassword: Boolean(suPassword), | ||
| 60 | hasConfirmPassword: Boolean(suConfirmPassword), | 61 | hasConfirmPassword: Boolean(suConfirmPassword), | ||
| 61 | hasCaptcha: Boolean(captchaAnswer), | 62 | hasCaptcha: Boolean(captchaAnswer), | ||
| 62 | - | hasTurnstileToken: Boolean(turnstileToken) | 63 | + | hasTurnstileToken: Boolean(turnstileToken), |
| 64 | + | isDev | |||
| 63 | }); | 65 | }); | ||
| 64 | return new Response(JSON.stringify({ error: 'All fields are required' } as RegisterResponseError), { status: 400 }); | 66 | return new Response(JSON.stringify({ error: 'All fields are required' } as RegisterResponseError), { status: 400 }); | ||
| 65 | } | 67 | } | ||
| 66 | 68 | ||||
| 67 | - | // Validate Turnstile token | 69 | + | // Validate Turnstile token (will be automatically bypassed in dev mode) |
| 68 | - | const isValidTurnstile = await validateTurnstileToken(turnstileToken, ip); | 70 | + | const isValidTurnstile = await validateTurnstileToken(turnstileToken || '', ip); |
| 69 | if (!isValidTurnstile) { | 71 | if (!isValidTurnstile) { | ||
| 70 | log.warn('Invalid Turnstile token', { maskedIp }); | 72 | log.warn('Invalid Turnstile token', { maskedIp }); | ||
| 71 | return new Response(JSON.stringify({ error: 'Security check failed. Please try again.' } as RegisterResponseError), { status: 400 }); | 73 | return new Response(JSON.stringify({ error: 'Security check failed. Please try again.' } as RegisterResponseError), { status: 400 }); | ||
M src/routes/api/session/login/+server.ts +10 -5
| @@ -11,6 +11,7 @@ import { createLogger } from '$lib/utils/logger.server'; | |||
| 11 | 11 | import { validateTurnstileToken } from '$lib/utils/turnstile.server'; | |
| 12 | 12 | ||
| 13 | 13 | const log = createLogger('login-server'); | |
| 14 | + | const isDev = process.env.NODE_ENV === 'development'; | |
| 14 | 15 | ||
| 15 | 16 | // In-memory map to track failed login attempts per IP | |
| 16 | 17 | const loginAttempts = new Map<string, { count: number, lastAttempt: number }>(); | |
| @@ -75,7 +76,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 75 | 76 | const cookieHeader = request.headers.get('cookie'); | |
| 76 | 77 | const clearanceCookie = getClearanceCookie(cookieHeader); | |
| 77 | 78 | ||
| 78 | - | // Note: Mark turnstileToken as optional if a valid clearance cookie exists. | |
| 79 | + | // Note: Mark turnstileToken as optional if a valid clearance cookie exists or in dev mode. | |
| 79 | 80 | const { username, password, turnstileToken } = body as { | |
| 80 | 81 | username: string; | |
| 81 | 82 | password: string; | |
| @@ -85,12 +86,13 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 85 | 86 | if ( | |
| 86 | 87 | typeof username !== 'string' || | |
| 87 | 88 | typeof password !== 'string' || | |
| 88 | - | (!clearanceCookie && typeof turnstileToken !== 'string') | |
| 89 | + | (!isDev && !clearanceCookie && typeof turnstileToken !== 'string') | |
| 89 | 90 | ) { | |
| 90 | 91 | log.warn('Missing or invalid input fields', { | |
| 91 | 92 | hasUsername: typeof username === 'string', | |
| 92 | 93 | hasPassword: typeof password === 'string', | |
| 93 | - | hasTurnstileToken: typeof turnstileToken === 'string' | |
| 94 | + | hasTurnstileToken: typeof turnstileToken === 'string', | |
| 95 | + | isDev | |
| 94 | 96 | }); | |
| 95 | 97 | return new Response( | |
| 96 | 98 | JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), | |
| @@ -98,9 +100,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||
| 98 | 100 | ); | |
| 99 | 101 | } | |
| 100 | 102 | ||
| 101 | - | // Check for clearance cookie before validating Turnstile token. | |
| 103 | + | // Check for dev mode or clearance cookie before validating Turnstile token. | |
| 102 | 104 | let isValidTurnstile = false; | |
| 103 | - | if (clearanceCookie) { | |
| 105 | + | if (isDev) { | |
| 106 | + | log.debug('Development mode: bypassing token challenge.'); | |
| 107 | + | isValidTurnstile = true; | |
| 108 | + | } else if (clearanceCookie) { | |
| 104 | 109 | log.debug('Clearance cookie detected; bypassing token challenge.'); | |
| 105 | 110 | isValidTurnstile = true; | |
| 106 | 111 | } else { | |
| @@ -11,6 +11,7 @@ import { createLogger } from '$lib/utils/logger.server'; | |||||
| 11 | import { validateTurnstileToken } from '$lib/utils/turnstile.server'; | 11 | import { validateTurnstileToken } from '$lib/utils/turnstile.server'; | ||
| 12 | 12 | ||||
| 13 | const log = createLogger('login-server'); | 13 | const log = createLogger('login-server'); | ||
| 14 | + | const isDev = process.env.NODE_ENV === 'development'; | |||
| 14 | 15 | ||||
| 15 | // In-memory map to track failed login attempts per IP | 16 | // In-memory map to track failed login attempts per IP | ||
| 16 | const loginAttempts = new Map<string, { count: number, lastAttempt: number }>(); | 17 | const loginAttempts = new Map<string, { count: number, lastAttempt: number }>(); | ||
| @@ -75,7 +76,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 75 | const cookieHeader = request.headers.get('cookie'); | 76 | const cookieHeader = request.headers.get('cookie'); | ||
| 76 | const clearanceCookie = getClearanceCookie(cookieHeader); | 77 | const clearanceCookie = getClearanceCookie(cookieHeader); | ||
| 77 | 78 | ||||
| 78 | - | // Note: Mark turnstileToken as optional if a valid clearance cookie exists. | 79 | + | // Note: Mark turnstileToken as optional if a valid clearance cookie exists or in dev mode. |
| 79 | const { username, password, turnstileToken } = body as { | 80 | const { username, password, turnstileToken } = body as { | ||
| 80 | username: string; | 81 | username: string; | ||
| 81 | password: string; | 82 | password: string; | ||
| @@ -85,12 +86,13 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 85 | if ( | 86 | if ( | ||
| 86 | typeof username !== 'string' || | 87 | typeof username !== 'string' || | ||
| 87 | typeof password !== 'string' || | 88 | typeof password !== 'string' || | ||
| 88 | - | (!clearanceCookie && typeof turnstileToken !== 'string') | 89 | + | (!isDev && !clearanceCookie && typeof turnstileToken !== 'string') |
| 89 | ) { | 90 | ) { | ||
| 90 | log.warn('Missing or invalid input fields', { | 91 | log.warn('Missing or invalid input fields', { | ||
| 91 | hasUsername: typeof username === 'string', | 92 | hasUsername: typeof username === 'string', | ||
| 92 | hasPassword: typeof password === 'string', | 93 | hasPassword: typeof password === 'string', | ||
| 93 | - | hasTurnstileToken: typeof turnstileToken === 'string' | 94 | + | hasTurnstileToken: typeof turnstileToken === 'string', |
| 95 | + | isDev | |||
| 94 | }); | 96 | }); | ||
| 95 | return new Response( | 97 | return new Response( | ||
| 96 | JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), | 98 | JSON.stringify({ error: 'Missing or invalid input fields' } as LoginResponseError), | ||
| @@ -98,9 +100,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => { | |||||
| 98 | ); | 100 | ); | ||
| 99 | } | 101 | } | ||
| 100 | 102 | ||||
| 101 | - | // Check for clearance cookie before validating Turnstile token. | 103 | + | // Check for dev mode or clearance cookie before validating Turnstile token. |
| 102 | let isValidTurnstile = false; | 104 | let isValidTurnstile = false; | ||
| 103 | - | if (clearanceCookie) { | 105 | + | if (isDev) { |
| 106 | + | log.debug('Development mode: bypassing token challenge.'); | |||
| 107 | + | isValidTurnstile = true; | |||
| 108 | + | } else if (clearanceCookie) { | |||
| 104 | log.debug('Clearance cookie detected; bypassing token challenge.'); | 109 | log.debug('Clearance cookie detected; bypassing token challenge.'); | ||
| 105 | isValidTurnstile = true; | 110 | isValidTurnstile = true; | ||
| 106 | } else { | 111 | } else { | ||