my own indieAuth provider! indiko.dunkirk.sh/docs
indieauth oauth2-server
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: prevent bad ids from auto registering

+40 -22
+40 -22
src/routes/indieauth.ts
··· 94 94 return hash === challenge; 95 95 } 96 96 97 - // Auto-register app if it doesn't exist 98 - function ensureApp(clientId: string, redirectUri: string): void { 97 + // Auto-register app if it doesn't exist (only for valid URLs, not generated client_ids) 98 + function ensureApp(clientId: string, redirectUri: string): { error?: string; app?: { name: string | null; redirect_uris: string } } { 99 99 const existing = db 100 - .query("SELECT id FROM apps WHERE client_id = ?") 101 - .get(clientId); 100 + .query("SELECT name, redirect_uris FROM apps WHERE client_id = ?") 101 + .get(clientId) as { name: string | null; redirect_uris: string } | undefined; 102 102 103 103 if (!existing) { 104 + // Only allow auto-registration for valid URLs (IndieAuth standard) 105 + // Reject generated client_ids like "ikc_xxxxx" 106 + try { 107 + new URL(clientId); 108 + } catch { 109 + return { error: "Client ID must be a valid URL for auto-registration. Non-URL clients must be pre-registered by an admin." }; 110 + } 111 + 104 112 // New app - auto-register (without pre-registration, no client secret or role) 105 113 db.query( 106 114 "INSERT INTO apps (client_id, redirect_uris, is_preregistered, first_seen, last_used) VALUES (?, ?, 0, ?, ?)", 107 115 ).run(clientId, JSON.stringify([redirectUri]), Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000)); 108 - } else { 109 - // Update last_used 110 - db.query("UPDATE apps SET last_used = ? WHERE client_id = ?").run( 111 - Math.floor(Date.now() / 1000), 112 - clientId, 113 - ); 116 + 117 + // Fetch the newly created app 118 + const newApp = db 119 + .query("SELECT name, redirect_uris FROM apps WHERE client_id = ?") 120 + .get(clientId) as { name: string | null; redirect_uris: string }; 121 + 122 + return { app: newApp }; 114 123 } 124 + 125 + // Update last_used 126 + db.query("UPDATE apps SET last_used = ? WHERE client_id = ?").run( 127 + Math.floor(Date.now() / 1000), 128 + clientId, 129 + ); 130 + 131 + return { app: existing }; 115 132 } 116 133 117 134 // GET /auth/authorize - Authorization request ··· 241 258 }); 242 259 } 243 260 244 - // Auto-register app or get existing app 245 - ensureApp(clientId, redirectUri); 246 - 247 - // Validate redirect_uri is in app's allowed list 248 - const app = db 249 - .query("SELECT name, redirect_uris FROM apps WHERE client_id = ?") 250 - .get(clientId) as { name: string | null; redirect_uris: string } | undefined; 251 - 252 - if (!app) { 253 - return new Response("App not found", { status: 400 }); 261 + // Verify app is registered 262 + const appResult = ensureApp(clientId, redirectUri); 263 + 264 + if (appResult.error) { 265 + return new Response(appResult.error, { status: 400 }); 254 266 } 267 + 268 + const app = appResult.app!; 255 269 256 270 const allowedRedirects = JSON.parse(app.redirect_uris) as string[]; 257 271 if (!allowedRedirects.includes(redirectUri)) { ··· 360 374 return Response.redirect(`/login?return=${encodeURIComponent(returnUrl)}`); 361 375 } 362 376 363 - // Auto-register app 364 - ensureApp(clientId, redirectUri); 377 + // Verify app is registered 378 + const appCheckResult = ensureApp(clientId, redirectUri); 379 + 380 + if (appCheckResult.error) { 381 + return new Response(appCheckResult.error, { status: 400 }); 382 + } 365 383 366 384 // Check if user has previously granted permission to this app 367 385 const permission = db