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

feat: use nanoid for client ID and secret generation

- Generate random client IDs with ikc_ prefix (indiko client)
- Generate random client secrets with iks_ prefix (indiko secret)
- Remove client ID input requirement from admin UI
- Auto-generate both credentials for pre-registered apps
- Display both in modal after creation with separate copy buttons

dunkirk.sh 25b6a876 8d4aa39a

verified
+15 -23
+3
bun.lock
··· 8 8 "@simplewebauthn/browser": "^13.2.2", 9 9 "@simplewebauthn/server": "^13.2.2", 10 10 "bun-sqlite-migrations": "^1.0.2", 11 + "nanoid": "^5.1.6", 11 12 }, 12 13 "devDependencies": { 13 14 "@simplewebauthn/types": "^12.0.0", ··· 62 63 "bun-sqlite-migrations": ["bun-sqlite-migrations@1.0.2", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-WLw8q67KM+1RN7o4DqVVhmJASypuBp8fygrfA8QD5HZEjiP+E5hD1SV2dpyB7A4tFqLdUF8cdln7+Ptj5+Hz1Q=="], 63 64 64 65 "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 66 + 67 + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], 65 68 66 69 "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], 67 70
+2 -1
package.json
··· 18 18 "dependencies": { 19 19 "@simplewebauthn/browser": "^13.2.2", 20 20 "@simplewebauthn/server": "^13.2.2", 21 - "bun-sqlite-migrations": "^1.0.2" 21 + "bun-sqlite-migrations": "^1.0.2", 22 + "nanoid": "^5.1.6" 22 23 } 23 24 }
+10 -22
src/routes/clients.ts
··· 1 1 import { db } from "../db"; 2 2 import crypto from "crypto"; 3 + import { nanoid } from "nanoid"; 3 4 4 5 function hashSecret(secret: string): string { 5 6 return crypto.createHash("sha256").update(secret).digest("hex"); 6 7 } 7 8 8 9 function generateClientSecret(): string { 9 - return crypto.randomBytes(32).toString("base64url"); 10 + return `iks_${nanoid(43)}`; // indiko secret 11 + } 12 + 13 + function generateClientId(): string { 14 + return `ikc_${nanoid(21)}`; // indiko client 10 15 } 11 16 12 17 function getSessionUser(req: Request): { username: string; userId: number; is_admin: boolean } | Response { ··· 131 136 132 137 try { 133 138 const body = await req.json(); 134 - const { clientId, name, logoUrl, description, redirectUris, availableRoles, defaultRole } = body; 135 - 136 - if (!clientId || typeof clientId !== "string") { 137 - return Response.json({ error: "Client ID is required" }, { status: 400 }); 138 - } 139 - 140 - try { 141 - new URL(clientId); 142 - } catch { 143 - return Response.json({ error: "Client ID must be a valid URL" }, { status: 400 }); 144 - } 139 + const { name, logoUrl, description, redirectUris, availableRoles, defaultRole } = body; 145 140 146 141 if (!redirectUris || !Array.isArray(redirectUris) || redirectUris.length === 0) { 147 142 return Response.json({ error: "At least one redirect URI is required" }, { status: 400 }); ··· 155 150 } 156 151 } 157 152 158 - const existing = db 159 - .query("SELECT id FROM apps WHERE client_id = ?") 160 - .get(clientId); 161 - 162 - if (existing) { 163 - return Response.json({ error: "Client ID already exists" }, { status: 409 }); 164 - } 165 - 166 - // Generate client secret for pre-registered clients 153 + // Generate client ID and secret for pre-registered clients 154 + const clientId = generateClientId(); 167 155 const clientSecret = generateClientSecret(); 168 156 const clientSecretHash = hashSecret(clientSecret); 169 157 ··· 205 193 id: result.lastInsertRowid, 206 194 clientId, 207 195 clientSecret, // Return the plain secret only once on creation 208 - name: name || new URL(clientId).hostname, 196 + name: name || clientId, 209 197 logoUrl: logoUrl || null, 210 198 description: description || null, 211 199 redirectUris,