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.

bug: fix iss handling

+56 -14
src/migrations/004_add_me_to_authcodes.sql src/migrations/002_add_me_to_authcodes.sql
+56 -14
src/routes/indieauth.ts
··· 942 942 "UPDATE permissions SET last_used = ? WHERE user_id = ? AND client_id = ?", 943 943 ).run(Math.floor(Date.now() / 1000), user.userId, clientId); 944 944 945 - return Response.redirect(`${redirectUri}?code=${code}&state=${state}`); 945 + const origin = process.env.ORIGIN || "http://localhost:3000"; 946 + return Response.redirect(`${redirectUri}?code=${code}&state=${state}&iss=${encodeURIComponent(origin)}`); 946 947 } 947 948 } 948 949 ··· 1303 1304 1304 1305 // POST /auth/authorize - Consent form submission 1305 1306 export async function authorizePost(req: Request): Promise<Response> { 1307 + const contentType = req.headers.get("Content-Type"); 1308 + 1309 + // Parse the request body 1310 + let body: Record<string, string>; 1311 + let formData: FormData; 1312 + 1313 + if (contentType?.includes("application/x-www-form-urlencoded")) { 1314 + formData = await req.formData(); 1315 + body = Object.fromEntries(formData.entries()) as Record<string, string>; 1316 + } else { 1317 + body = await req.json(); 1318 + // Create a fake FormData for JSON requests 1319 + formData = new FormData(); 1320 + Object.entries(body).forEach(([key, value]) => { 1321 + formData.append(key, value); 1322 + }); 1323 + } 1324 + 1325 + const grantType = body.grant_type; 1326 + 1327 + // If grant_type is present, this is a token exchange request (IndieAuth profile scope only) 1328 + if (grantType === "authorization_code") { 1329 + // Create a mock request for token() function 1330 + const mockReq = new Request(req.url, { 1331 + method: "POST", 1332 + headers: req.headers, 1333 + body: contentType?.includes("application/x-www-form-urlencoded") 1334 + ? new URLSearchParams(body).toString() 1335 + : JSON.stringify(body), 1336 + }); 1337 + return token(mockReq); 1338 + } 1339 + 1340 + // Otherwise it's a consent form submission 1306 1341 const user = getUserFromCookie(req); 1307 1342 1308 1343 if (!user) { 1309 1344 return new Response("Unauthorized", { status: 401 }); 1310 1345 } 1311 1346 1312 - const formData = await req.formData(); 1313 - const action = formData.get("action") as string; 1314 - const clientId = formData.get("client_id") as string; 1315 - const redirectUri = formData.get("redirect_uri") as string; 1316 - const state = formData.get("state") as string; 1317 - const codeChallenge = formData.get("code_challenge") as string; 1318 - const me = formData.get("me") as string | null; 1347 + const action = body.action; 1348 + const clientId = body.client_id; 1349 + const redirectUri = body.redirect_uri; 1350 + const state = body.state; 1351 + const codeChallenge = body.code_challenge; 1352 + const me = body.me || null; 1319 1353 1320 1354 if (!clientId || !redirectUri || !state || !codeChallenge) { 1321 1355 return new Response("Missing required parameters", { status: 400 }); ··· 1340 1374 const expiresAt = Math.floor(Date.now() / 1000) + 60; // 60 seconds 1341 1375 1342 1376 db.query( 1343 - "INSERT INTO authcodes (code, user_id, client_id, redirect_uri, scopes, code_challenge, expires_at, me) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", 1377 + "INSERT INTO authcodes (code, user_id, client_id, redirect_uri, scopes, code_challenge, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", 1344 1378 ).run( 1345 1379 code, 1346 1380 user.userId, ··· 1349 1383 JSON.stringify(approvedScopes), 1350 1384 codeChallenge, 1351 1385 expiresAt, 1352 - me, 1353 1386 ); 1354 1387 1355 1388 // Store or update permission grant ··· 1515 1548 // Look up authorization code 1516 1549 const authcode = db 1517 1550 .query( 1518 - "SELECT user_id, client_id, redirect_uri, scopes, code_challenge, expires_at, used, me FROM authcodes WHERE code = ?", 1551 + "SELECT user_id, client_id, redirect_uri, scopes, code_challenge, expires_at, used FROM authcodes WHERE code = ?", 1519 1552 ) 1520 1553 .get(code) as 1521 1554 | { ··· 1526 1559 code_challenge: string; 1527 1560 expires_at: number; 1528 1561 used: number; 1529 - me: string | null; 1530 1562 } 1531 1563 | undefined; 1532 1564 ··· 1647 1679 meValue = user.url; 1648 1680 } 1649 1681 1682 + const origin = process.env.ORIGIN || "http://localhost:3000"; 1683 + 1650 1684 const response: Record<string, unknown> = { 1651 1685 me: meValue, 1652 1686 profile, 1653 1687 scope: scopes.join(" "), 1654 - iss: process.env.ORIGIN || "http://localhost:3000", 1688 + iss: origin, 1655 1689 }; 1656 1690 1657 1691 // Include role if assigned ··· 1659 1693 response.role = permission.role; 1660 1694 } 1661 1695 1662 - return Response.json(response); 1696 + 1697 + 1698 + return Response.json(response, { 1699 + headers: { 1700 + "Content-Type": "application/json", 1701 + "Cache-Control": "no-store", 1702 + "Pragma": "no-cache", 1703 + }, 1704 + }); 1663 1705 } catch (error) { 1664 1706 console.error("Token exchange error:", error); 1665 1707 return Response.json(