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: allow custom domains for indieauth

+22 -5
+2 -1
CRUSH.md
··· 56 56 - User profile endpoints with h-card microformats 57 57 - Token exchange endpoint for client apps 58 58 - Invite-based registration for new users (admin only) 59 + - **`me` parameter delegation**: When a client passes `me=https://example.com` in the authorization request and it matches the user's website URL, the token response returns that URL instead of the canonical `/u/{username}` URL 59 60 60 61 ### Database Schema 61 62 - **users**: username, name, email, photo, url, status, role, is_admin ··· 64 65 - **challenges**: WebAuthn challenges (5-minute expiry) 65 66 - **apps**: auto-registered OAuth clients 66 67 - **permissions**: per-user, per-app granted scopes 67 - - **authcodes**: short-lived authorization codes (60-second expiry, single-use) 68 + - **authcodes**: short-lived authorization codes (60-second expiry, single-use), includes `me` parameter for delegation 68 69 - **invites**: admin-created invite codes 69 70 70 71 ### WebAuthn/Passkey Settings
+2
src/migrations/004_add_me_to_authcodes.sql
··· 1 + -- Add me parameter to authcodes for IndieAuth client delegation 2 + ALTER TABLE authcodes ADD COLUMN me TEXT;
+18 -4
src/routes/indieauth.ts
··· 144 144 const codeChallenge = params.get("code_challenge"); 145 145 const codeChallengeMethod = params.get("code_challenge_method"); 146 146 const scope = params.get("scope") || "profile"; 147 + const me = params.get("me"); 147 148 148 149 if (responseType !== "code") { 149 150 return new Response("Unsupported response_type", { status: 400 }); ··· 401 402 const expiresAt = Math.floor(Date.now() / 1000) + 60; // 60 seconds 402 403 403 404 db.query( 404 - "INSERT INTO authcodes (code, user_id, client_id, redirect_uri, scopes, code_challenge, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", 405 + "INSERT INTO authcodes (code, user_id, client_id, redirect_uri, scopes, code_challenge, expires_at, me) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", 405 406 ).run( 406 407 code, 407 408 user.userId, ··· 410 411 JSON.stringify(requestedScopes), 411 412 codeChallenge, 412 413 expiresAt, 414 + me, 413 415 ); 414 416 415 417 // Update permission last_used ··· 429 431 state, 430 432 codeChallenge, 431 433 requestedScopes, 434 + me, 432 435 ); 433 436 } 434 437 ··· 439 442 state: string, 440 443 codeChallenge: string, 441 444 scopes: string[], 445 + me: string | null, 442 446 ): Response { 443 447 // Load app metadata if pre-registered 444 448 const appData = db ··· 745 749 <input type="hidden" name="redirect_uri" value="${redirectUri}" /> 746 750 <input type="hidden" name="state" value="${state}" /> 747 751 <input type="hidden" name="code_challenge" value="${codeChallenge}" /> 752 + ${me ? `<input type="hidden" name="me" value="${me}" />` : ""} 748 753 <!-- Always include profile scope as it's required --> 749 754 <input type="hidden" name="scope" value="profile" /> 750 755 ··· 776 781 const redirectUri = formData.get("redirect_uri") as string; 777 782 const state = formData.get("state") as string; 778 783 const codeChallenge = formData.get("code_challenge") as string; 784 + const me = formData.get("me") as string | null; 779 785 780 786 if (!clientId || !redirectUri || !state || !codeChallenge) { 781 787 return new Response("Missing required parameters", { status: 400 }); ··· 800 806 const expiresAt = Math.floor(Date.now() / 1000) + 60; // 60 seconds 801 807 802 808 db.query( 803 - "INSERT INTO authcodes (code, user_id, client_id, redirect_uri, scopes, code_challenge, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", 809 + "INSERT INTO authcodes (code, user_id, client_id, redirect_uri, scopes, code_challenge, expires_at, me) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", 804 810 ).run( 805 811 code, 806 812 user.userId, ··· 809 815 JSON.stringify(approvedScopes), 810 816 codeChallenge, 811 817 expiresAt, 818 + me, 812 819 ); 813 820 814 821 // Store or update permission grant ··· 953 960 // Look up authorization code 954 961 const authcode = db 955 962 .query( 956 - "SELECT user_id, client_id, redirect_uri, scopes, code_challenge, expires_at, used FROM authcodes WHERE code = ?", 963 + "SELECT user_id, client_id, redirect_uri, scopes, code_challenge, expires_at, used, me FROM authcodes WHERE code = ?", 957 964 ) 958 965 .get(code) as 959 966 | { ··· 964 971 code_challenge: string; 965 972 expires_at: number; 966 973 used: number; 974 + me: string | null; 967 975 } 968 976 | undefined; 969 977 ··· 1081 1089 .query("SELECT role FROM permissions WHERE user_id = ? AND client_id = ?") 1082 1090 .get(authcode.user_id, client_id) as { role: string | null } | undefined; 1083 1091 1092 + // Use the me parameter from authorization if it matches user's website, otherwise use canonical URL 1093 + let meValue = `${process.env.ORIGIN}/u/${user.username}`; 1094 + if (authcode.me && user.url && authcode.me === user.url) { 1095 + meValue = authcode.me; 1096 + } 1097 + 1084 1098 const response: Record<string, unknown> = { 1085 - me: `${process.env.ORIGIN}/u/${user.username}`, 1099 + me: meValue, 1086 1100 profile, 1087 1101 scope: scopes.join(" "), 1088 1102 };