Changeset d85b66f in Klonkt for src/services/OpenWebAuthService.js
- Timestamp:
- 08/18/2026 06:50:08 PM (3 weeks ago)
- Branches:
- main
- Children:
- 192fe28
- Parents:
- af2cc73
- git-author:
- Bart <bart@…> (08/18/2026 06:21:21 PM)
- git-committer:
- Bart <bart@…> (08/18/2026 06:50:08 PM)
- File:
-
- 1 edited
-
src/services/OpenWebAuthService.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/OpenWebAuthService.js
raf2cc73 rd85b66f 159 159 } 160 160 161 // ── de HOME-kant: onze gebruiker bewijst zich elders ────────────────────── 162 // 163 // Hier zijn de rollen omgedraaid. Wij hebben nu de prive-sleutel nodig -- om te 164 // ondertekenen en om het token te ontsleutelen -- en dat is precies waarom 165 // alleen een echte instance deze kant kan spelen. 166 167 /** `bdest` terug naar een URL. Hex in, URL uit; ongeldig = null. */ 168 export function fromBdest(hex) { 169 const h = String(hex || ''); 170 if (!/^[0-9a-f]+$/i.test(h) || h.length % 2) return null; 171 try { 172 const u = new URL(Buffer.from(h, 'hex').toString('utf8')); 173 if (u.protocol !== 'https:' && u.protocol !== 'http:') return null; 174 return u; 175 } catch { return null; } 176 } 177 178 /** 179 * Het token-endpoint van de doelsite, gevonden via webfinger op zijn WORTEL. 180 * 181 * En meteen de open-redirect-verdediging van deze kant: het gevonden endpoint 182 * moet dezelfde origin hebben als `bdest`. De FEP zegt het met zoveel woorden -- 183 * lukt de ontdekking niet, of wijst hij ergens anders heen, dan sturen we de 184 * browser NIET naar bdest maar geven we een fout. Anders is /magic het 185 * doorgeefluik. 186 */ 187 export async function discoverTokenEndpoint(bdestUrl, { fetchImpl = fetch } = {}) { 188 let origin; 189 try { origin = new URL(bdestUrl).origin; } catch { return null; } 190 const url = `${origin}/.well-known/webfinger?resource=${encodeURIComponent(origin + '/')}`; 191 try { 192 const r = await fetchImpl(url, { headers: { accept: 'application/jrd+json, application/json' } }); 193 if (!r.ok) return null; 194 const jrd = await r.json(); 195 const link = (jrd.links || []).find((l) => l && l.rel === REL_TOKEN && l.href); 196 if (!link) return null; 197 if (new URL(link.href).origin !== origin) return null; // open redirect 198 return link.href; 199 } catch { return null; } 200 } 201 202 /** 203 * Het token ophalen bij de doelsite, ondertekend namens onze actor. 204 * 205 * De handtekening gaat in `Authorization: Signature ...` -- zo schrijft de FEP 206 * het voor, en niet in de `Signature`-header die de rest van de fediverse 207 * gebruikt. Plus `X-Open-Web-Auth` met willekeur erin: de doelsite doet er 208 * niets mee, het voegt alleen entropie toe aan wat we ondertekenen. 209 */ 210 export async function requestToken(endpoint, { keyId, privatePem, fetchImpl = fetch } = {}) { 211 const u = new URL(endpoint); 212 const date = new Date().toUTCString(); 213 const nonce = crypto.randomBytes(16).toString('hex'); 214 const target = `${u.pathname}${u.search || ''}`; 215 const signingString = [ 216 `(request-target): get ${target}`, 217 `host: ${u.host}`, 218 `date: ${date}`, 219 `x-open-web-auth: ${nonce}`, 220 ].join('\n'); 221 const signature = crypto.sign('sha256', Buffer.from(signingString), privatePem).toString('base64'); 222 const headers = { 223 Accept: 'application/json', 224 Date: date, 225 'X-Open-Web-Auth': nonce, 226 Authorization: `Signature keyId="${keyId}",algorithm="rsa-sha256",headers="(request-target) host date x-open-web-auth",signature="${signature}"`, 227 }; 228 const r = await fetchImpl(endpoint, { headers }); 229 if (!r.ok) return null; 230 const j = await r.json(); 231 if (!j || j.success !== true || !j.encrypted_token) return null; 232 return String(j.encrypted_token); 233 } 234 235 /** Het token uitpakken met onze eigen prive-sleutel. */ 236 export function decryptToken(encrypted, privatePem) { 237 const b64 = String(encrypted || '').replace(/-/g, '+').replace(/_/g, '/'); 238 const buf = crypto.privateDecrypt( 239 { key: privatePem, padding: crypto.constants.RSA_PKCS1_PADDING }, 240 Buffer.from(b64, 'base64'), 241 ); 242 const t = buf.toString('utf8'); 243 // Een token is URL-veilige tekst. Bij een verkeerde sleutel geeft PKCS#1 v1.5 244 // geen fout maar afgeleide onzin terug (implicit rejection, zie de toelichting 245 // bij encryptTokenFor), en die onzin hoort hier te stranden in plaats van als 246 // token de wereld in te gaan. 247 // 248 // ALLEBEI de voorwaarden doen werk, en dat is gemeten met 300 vreemde sleutels: 249 // - de TEKENSET vangt vrijwel alles. Van die 300 was er geen enkele die 250 // volledig uit URL-veilige tekens bestond. 251 // - de ONDERGRENS vangt de rest. De onzin heeft een willekeurige lengte (5 252 // tot 209 bytes gezien), en 18 van de 300 was korter dan 16 bytes. Bij zo'n 253 // kort stukje is "toevallig allemaal URL-veilig" niet meer verwaarloosbaar: 254 // per byte is die kans ruwweg een kwart. 255 // Zestien is daarmee geen rond getal maar een grens die iets doet. Een echte 256 // implementatie zit er ruim boven (de onze: 43 tekens). 257 return /^[A-Za-z0-9._~-]{16,512}$/.test(t) ? t : null; 258 } 259 161 260 // ── wie is er binnen ────────────────────────────────────────────────────── 162 261 … … 197 296 parseHandle, discoverRedirectEndpoint, toBdest, buildRedirect, 198 297 guestActor, isFollowerOf, viewerFor, 298 fromBdest, discoverTokenEndpoint, requestToken, decryptToken, 199 299 };
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)