From 550cadcb068136de15c3fb67882be201668c91ea Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:49:09 +0200 Subject: [PATCH] fix: harden auth, cron secrets, and provider flows (GNU-17) (#148) - Replace === with crypto.timingSafeEqual in all 7 cron routes via shared lib/auth/cron.ts - Add in-memory rate limiting (60 req/min) and expires_at support to calendar feed - Add exponential backoff on MFA verify after 3 failed attempts - Add 60s cooldown on password reset requests - Validate bank callback auth code format before API call - Redact session IDs from bank sync and callback logs - Validate OAuth redirect_uris against allowlist (claude.ai, claude.com, localhost) - Remove excessive PII/debug console logging from login page Co-authored-by: Claude Opus 4.6 (1M context) --- app/(auth)/login/page.tsx | 86 +++++------------- app/(auth)/mfa/verify/page.tsx | 30 ++++++- .../admin/seed-template-embeddings/route.ts | 9 +- app/api/calendar/feed/[token]/route.ts | 34 +++++++ app/api/deadlines/status/cron/route.ts | 10 +-- app/api/documents/verify/cron/route.ts | 10 +-- .../enable-banking/callback/route.ts | 10 ++- .../enable-banking/sync/cron/route.ts | 12 +-- app/api/invoices/reminders/cron/route.ts | 29 +----- .../register/__tests__/route.test.ts | 88 +++++++++++++++++++ app/api/mcp-oauth/register/route.ts | 27 +++++- app/api/sandbox/cleanup/cron/route.ts | 9 +- app/api/tax-deadlines/cron/route.ts | 10 +-- lib/auth/__tests__/cron.test.ts | 64 ++++++++++++++ lib/auth/cron.ts | 32 +++++++ .../20260329120000_calendar_feed_expiry.sql | 6 ++ 16 files changed, 330 insertions(+), 136 deletions(-) create mode 100644 app/api/mcp-oauth/register/__tests__/route.test.ts create mode 100644 lib/auth/__tests__/cron.test.ts create mode 100644 lib/auth/cron.ts create mode 100644 supabase/migrations/20260329120000_calendar_feed_expiry.sql diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index f61eef60..b0ccbac0 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { createClient } from '@/lib/supabase/client' @@ -18,10 +18,25 @@ export default function LoginPage() { const [isLoading, setIsLoading] = useState(false) const [isEmailSent, setIsEmailSent] = useState(false) const [showResetPassword, setShowResetPassword] = useState(false) + const [resetCooldownUntil, setResetCooldownUntil] = useState(null) + const [resetCooldownRemaining, setResetCooldownRemaining] = useState(0) const { toast } = useToast() const router = useRouter() const supabase = createClient() + // Reset cooldown timer + useEffect(() => { + if (!resetCooldownUntil) return + const tick = () => { + const remaining = Math.max(0, Math.ceil((resetCooldownUntil - Date.now()) / 1000)) + setResetCooldownRemaining(remaining) + if (remaining <= 0) setResetCooldownUntil(null) + } + tick() + const interval = setInterval(tick, 1000) + return () => clearInterval(interval) + }, [resetCooldownUntil]) + const handlePasswordLogin = async (e: React.FormEvent) => { e.preventDefault() setIsLoading(true) @@ -31,27 +46,12 @@ export default function LoginPage() { const passwordValue = (formData.get('password') as string) || password try { - console.log('[login] attempting signInWithPassword', { - email: emailValue, - hasPassword: !!passwordValue, - supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL, - }) - - const { data, error } = await supabase.auth.signInWithPassword({ + const { error } = await supabase.auth.signInWithPassword({ email: emailValue, password: passwordValue, }) if (error) { - console.error('[login] signInWithPassword error', { - message: error.message, - code: error.code, - status: error.status, - name: error.name, - stack: error.stack, - cause: error.cause, - fullError: JSON.stringify(error, Object.getOwnPropertyNames(error)), - }) toast({ title: 'Inloggning misslyckades', description: error.message === 'Invalid login credentials' @@ -62,24 +62,8 @@ export default function LoginPage() { return } - console.log('[login] signInWithPassword success', { - userId: data.user?.id, - email: data.user?.email, - hasSession: !!data.session, - provider: data.user?.app_metadata?.provider, - }) - // Check MFA status - const { data: aal, error: mfaError } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel() - if (mfaError) { - console.error('[login] MFA check error', { - message: mfaError.message, - code: mfaError.code, - status: mfaError.status, - fullError: JSON.stringify(mfaError, Object.getOwnPropertyNames(mfaError)), - }) - } - console.log('[login] MFA status', { currentLevel: aal?.currentLevel, nextLevel: aal?.nextLevel }) + const { data: aal } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel() if (aal?.nextLevel === 'aal2' && aal?.currentLevel === 'aal1') { router.push('/mfa/verify') @@ -89,13 +73,6 @@ export default function LoginPage() { router.push('/') router.refresh() } catch (error) { - console.error('[login] unexpected exception', { - error, - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - type: typeof error, - constructor: error?.constructor?.name, - }) toast({ title: 'Inloggning misslyckades', description: getErrorMessage(error, { context: 'auth' }), @@ -114,25 +91,11 @@ export default function LoginPage() { const emailValue = (formData.get('email') as string) || email try { - console.log('[login] attempting resetPasswordForEmail', { - email: emailValue, - redirectTo: `${window.location.origin}/auth/callback?next=/reset-password`, - }) - const { error } = await supabase.auth.resetPasswordForEmail(emailValue, { redirectTo: `${window.location.origin}/auth/callback?next=/reset-password`, }) if (error) { - console.error('[login] resetPasswordForEmail error', { - message: error.message, - code: error.code, - status: error.status, - name: error.name, - stack: error.stack, - cause: error.cause, - fullError: JSON.stringify(error, Object.getOwnPropertyNames(error)), - }) toast({ title: 'Kunde inte skicka återställningslänk', description: getErrorMessage(error, { context: 'auth' }), @@ -141,21 +104,14 @@ export default function LoginPage() { return } - console.log('[login] resetPasswordForEmail success', { email: emailValue }) setEmail(emailValue) + setResetCooldownUntil(Date.now() + 60_000) setIsEmailSent(true) toast({ title: 'Återställningslänk skickad!', description: 'Kolla din inkorg för att återställa lösenordet.', }) } catch (error) { - console.error('[login] resetPasswordForEmail unexpected exception', { - error, - message: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - type: typeof error, - constructor: error?.constructor?.name, - }) toast({ title: 'Kunde inte skicka återställningslänk', description: getErrorMessage(error, { context: 'auth' }), @@ -242,12 +198,14 @@ export default function LoginPage() { className="h-11" /> -