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.

feat: validate redirect url

+209
+209
src/routes/indieauth.ts
··· 130 130 return new Response("Missing required parameters", { status: 400 }); 131 131 } 132 132 133 + // Validate redirect_uri is a valid URL 134 + try { 135 + new URL(redirectUri); 136 + } catch { 137 + return new Response( 138 + `<!DOCTYPE html> 139 + <html lang="en"> 140 + <head> 141 + <meta charset="UTF-8"> 142 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 143 + <title>Invalid Redirect URI • Indiko</title> 144 + <link rel="preconnect" href="https://fonts.googleapis.com"> 145 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 146 + <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet"> 147 + <style> 148 + :root { 149 + --mahogany: #26242b; 150 + --lavender: #d9d0de; 151 + --old-rose: #bc8da0; 152 + --rosewood: #a04668; 153 + --berry-crush: #ab4967; 154 + } 155 + * { margin: 0; padding: 0; box-sizing: border-box; } 156 + body { 157 + font-family: "Space Grotesk", sans-serif; 158 + background: var(--mahogany); 159 + color: var(--lavender); 160 + min-height: 100vh; 161 + display: flex; 162 + align-items: center; 163 + justify-content: center; 164 + padding: 2rem; 165 + } 166 + .error-box { 167 + max-width: 500px; 168 + background: rgba(188, 141, 160, 0.05); 169 + border: 2px solid var(--rosewood); 170 + padding: 2.5rem; 171 + } 172 + h1 { 173 + font-size: 2rem; 174 + font-weight: 700; 175 + background: linear-gradient(135deg, var(--old-rose), var(--rosewood)); 176 + -webkit-background-clip: text; 177 + -webkit-text-fill-color: transparent; 178 + background-clip: text; 179 + margin-bottom: 1.5rem; 180 + letter-spacing: -0.05rem; 181 + } 182 + p { 183 + line-height: 1.8; 184 + margin-bottom: 1rem; 185 + color: var(--lavender); 186 + } 187 + code { 188 + background: rgba(12, 23, 19, 0.8); 189 + padding: 0.25rem 0.5rem; 190 + color: var(--berry-crush); 191 + font-size: 0.875rem; 192 + word-break: break-all; 193 + display: inline-block; 194 + max-width: 100%; 195 + } 196 + .error-details { 197 + background: rgba(160, 70, 104, 0.1); 198 + border-left: 4px solid var(--rosewood); 199 + padding: 1rem; 200 + margin-top: 1.5rem; 201 + } 202 + .error-details strong { 203 + display: block; 204 + margin-bottom: 0.5rem; 205 + color: var(--old-rose); 206 + } 207 + </style> 208 + </head> 209 + <body> 210 + <div class="error-box"> 211 + <h1>Invalid Redirect URI</h1> 212 + <p> 213 + The OAuth authorization request failed because the provided <code>redirect_uri</code> is not a valid URL. 214 + </p> 215 + <div class="error-details"> 216 + <strong>Provided redirect_uri:</strong> 217 + <code>${redirectUri}</code> 218 + </div> 219 + <p style="margin-top: 1.5rem; font-size: 0.875rem; color: var(--old-rose);"> 220 + The redirect URI must be a valid, absolute URL (e.g., https://example.com/callback). 221 + </p> 222 + </div> 223 + </body> 224 + </html>`, 225 + { 226 + status: 400, 227 + headers: { "Content-Type": "text/html" } 228 + } 229 + ); 230 + } 231 + 133 232 if (codeChallengeMethod && codeChallengeMethod !== "S256") { 134 233 return new Response("Only S256 code_challenge_method supported", { 135 234 status: 400, 136 235 }); 236 + } 237 + 238 + // Auto-register app or get existing app 239 + ensureApp(clientId, redirectUri); 240 + 241 + // Validate redirect_uri is in app's allowed list 242 + const app = db 243 + .query("SELECT name, redirect_uris FROM apps WHERE client_id = ?") 244 + .get(clientId) as { name: string | null; redirect_uris: string } | undefined; 245 + 246 + if (!app) { 247 + return new Response("App not found", { status: 400 }); 248 + } 249 + 250 + const allowedRedirects = JSON.parse(app.redirect_uris) as string[]; 251 + if (!allowedRedirects.includes(redirectUri)) { 252 + const appName = app.name || clientId; 253 + return new Response( 254 + `<!DOCTYPE html> 255 + <html lang="en"> 256 + <head> 257 + <meta charset="UTF-8"> 258 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 259 + <title>Unauthorized Redirect URI • Indiko</title> 260 + <link rel="preconnect" href="https://fonts.googleapis.com"> 261 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 262 + <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet"> 263 + <style> 264 + :root { 265 + --mahogany: #26242b; 266 + --lavender: #d9d0de; 267 + --old-rose: #bc8da0; 268 + --rosewood: #a04668; 269 + --berry-crush: #ab4967; 270 + } 271 + * { margin: 0; padding: 0; box-sizing: border-box; } 272 + body { 273 + font-family: "Space Grotesk", sans-serif; 274 + background: var(--mahogany); 275 + color: var(--lavender); 276 + min-height: 100vh; 277 + display: flex; 278 + align-items: center; 279 + justify-content: center; 280 + padding: 2rem; 281 + } 282 + .error-box { 283 + max-width: 600px; 284 + background: rgba(188, 141, 160, 0.05); 285 + border: 2px solid var(--rosewood); 286 + padding: 2.5rem; 287 + } 288 + h1 { 289 + font-size: 2rem; 290 + font-weight: 700; 291 + background: linear-gradient(135deg, var(--old-rose), var(--rosewood)); 292 + -webkit-background-clip: text; 293 + -webkit-text-fill-color: transparent; 294 + background-clip: text; 295 + margin-bottom: 1.5rem; 296 + letter-spacing: -0.05rem; 297 + } 298 + p { 299 + line-height: 1.8; 300 + margin-bottom: 1rem; 301 + color: var(--lavender); 302 + } 303 + code { 304 + background: rgba(12, 23, 19, 0.8); 305 + padding: 0.25rem 0.5rem; 306 + color: var(--berry-crush); 307 + font-size: 0.875rem; 308 + word-break: break-all; 309 + display: inline-block; 310 + max-width: 100%; 311 + } 312 + .error-details { 313 + background: rgba(160, 70, 104, 0.1); 314 + border-left: 4px solid var(--rosewood); 315 + padding: 1rem; 316 + margin: 1.5rem 0; 317 + } 318 + .error-details strong { 319 + display: block; 320 + margin-bottom: 0.5rem; 321 + color: var(--old-rose); 322 + } 323 + </style> 324 + </head> 325 + <body> 326 + <div class="error-box"> 327 + <h1>Unauthorized Redirect URI</h1> 328 + <p> 329 + The OAuth authorization request failed because the provided <code>redirect_uri</code> is not registered for this client application. 330 + </p> 331 + <div class="error-details"> 332 + <strong>Requested redirect_uri:</strong> 333 + <code>${redirectUri}</code> 334 + </div> 335 + <p style="margin-top: 1.5rem; font-size: 0.875rem; color: var(--old-rose);"> 336 + The redirect_uri must exactly match a registered URI for <strong>${appName}</strong>. If you are the application developer, please ensure your redirect_uri matches the one registered with this authorization server. 337 + </p> 338 + </div> 339 + </body> 340 + </html>`, 341 + { 342 + status: 400, 343 + headers: { "Content-Type": "text/html" } 344 + } 345 + ); 137 346 } 138 347 139 348 // Check if user is logged in