Browse Source

Unified cron secret auth

Updated edge functions to use async validateCronSecret with DB fallback,
and adjusted fetch-rss, update-feed, and purge-articles to support cron/internal auth.

X-Lovable-Edit-ID: edt-0f603a62-17c6-4df3-95b0-6292341125cc
gpt-engineer-app[bot] 3 days ago
parent
commit
6c4193faa1

+ 51 - 12
supabase/functions/_shared/security.ts

@@ -118,25 +118,64 @@ export async function verifySuperUser(req: Request): Promise<boolean> {
 
 /**
  * Validates a cron job secret for internal service-to-service calls
- * Cron jobs should use a shared secret for authentication
- * SECURITY: Fails closed - requires CRON_SECRET to be configured
+ * ASYNC version: checks env var first, then falls back to app_secrets table
+ * SECURITY: Fails closed - requires secret to be configured somewhere
  */
-export function validateCronSecret(req: Request): boolean {
+export async function validateCronSecret(req: Request): Promise<boolean> {
   const cronSecret = req.headers.get('x-cron-secret');
-  const expectedSecret = Deno.env.get('CRON_SECRET');
   
-  // SECURITY: Fail closed - require explicit configuration
-  if (!expectedSecret) {
-    console.error('CRON_SECRET environment variable not configured - denying access');
+  if (!cronSecret) {
     return false;
   }
-  
-  // Constant-time comparison to prevent timing attacks
-  if (cronSecret?.length !== expectedSecret.length) {
+
+  // 1. Try environment variable first (fastest)
+  const envSecret = Deno.env.get('CRON_SECRET');
+  if (envSecret) {
+    if (cronSecret.length !== envSecret.length) {
+      console.log('validateCronSecret: secret length mismatch (env)');
+      return false;
+    }
+    if (cronSecret === envSecret) {
+      return true;
+    }
+    console.log('validateCronSecret: secret value mismatch (env)');
+    return false;
+  }
+
+  // 2. Fallback: read from app_secrets table via service_role
+  console.log('validateCronSecret: CRON_SECRET not in env, trying app_secrets fallback');
+  try {
+    const supabase = createClient(
+      Deno.env.get('SUPABASE_URL')!,
+      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+    );
+
+    const { data, error } = await supabase
+      .from('app_secrets')
+      .select('value')
+      .eq('key', 'cron_secret')
+      .single();
+
+    if (error || !data?.value) {
+      console.error('validateCronSecret: cron_secret not found in app_secrets either - denying access');
+      return false;
+    }
+
+    const dbSecret = data.value;
+    if (cronSecret.length !== dbSecret.length) {
+      console.log('validateCronSecret: secret length mismatch (db)');
+      return false;
+    }
+    if (cronSecret === dbSecret) {
+      console.log('validateCronSecret: validated via app_secrets fallback');
+      return true;
+    }
+    console.log('validateCronSecret: secret value mismatch (db)');
+    return false;
+  } catch (err) {
+    console.error('validateCronSecret: error reading app_secrets:', err);
     return false;
   }
-  
-  return cronSecret === expectedSecret;
 }
 
 /**

+ 1 - 1
supabase/functions/fetch-rss/index.ts

@@ -73,7 +73,7 @@ serve(async (req) => {
 
   try {
     // Authentication: Allow internal calls (cron jobs) OR authenticated users
-    const isCronJob = validateCronSecret(req);
+    const isCronJob = await validateCronSecret(req);
     const isInternal = isInternalCall(req);
     const auth = await verifyAuth(req);
     

+ 1 - 1
supabase/functions/purge-articles/index.ts

@@ -26,7 +26,7 @@ serve(async (req) => {
 
   try {
     // Authentication: Allow cron jobs, internal calls, or super users
-    const isCronJob = validateCronSecret(req);
+    const isCronJob = await validateCronSecret(req);
     const isInternal = isInternalCall(req);
     const isSuperUser = await verifySuperUser(req);
     

+ 1 - 1
supabase/functions/update-feed/index.ts

@@ -14,7 +14,7 @@ serve(async (req) => {
 
   try {
     // Authentication: Allow cron jobs, internal calls, or super users
-    const isCronJob = validateCronSecret(req);
+    const isCronJob = await validateCronSecret(req);
     const isInternal = isInternalCall(req);
     const isSuperUser = await verifySuperUser(req);