| 1 | /**
|
|---|
| 2 | * OpenWebAuth (FEP-61cf) — de TARGET-kant.
|
|---|
| 3 | *
|
|---|
| 4 | * Waarom dit bestaat: `fan_only` betekent "mijn volgers op de fediverse", maar
|
|---|
| 5 | * de poort vroeg om een KLONKT-ACCOUNT. Dat is de verkeerde vraag: precies de
|
|---|
| 6 | * mensen voor wie de poort openstaat -- volgers elders -- konden er niet door,
|
|---|
| 7 | * en wie er wel door kon had meestal niets met volgen te maken. Hiermee kan een
|
|---|
| 8 | * bezoeker bewijzen dat hij @iemand@ergens is, zonder hier een account, een
|
|---|
| 9 | * wachtwoord of een cookie van een derde partij.
|
|---|
| 10 | *
|
|---|
| 11 | * WIJ ZIJN DE TARGET INSTANCE, nooit de home instance. Dat is de prettige helft:
|
|---|
| 12 | * de home instance heeft de prive-sleutel nodig (om te ondertekenen en om ons
|
|---|
| 13 | * token te ontsleutelen), wij hebben alleen publieke sleutels nodig. Er staat
|
|---|
| 14 | * hier dus geen geheim van iemand anders, en we kunnen ook niemands identiteit
|
|---|
| 15 | * uitgeven. Het spiegelbeeld (Klonkt-gebruikers laten inloggen OP andere sites,
|
|---|
| 16 | * de /magic-kant) is bewust NIET gebouwd: dat is een andere functie.
|
|---|
| 17 | *
|
|---|
| 18 | * De stroom, met de FEP-stappen erbij:
|
|---|
| 19 | * 1. bezoeker geeft zijn adres -> wij webfingeren hem, vinden zijn
|
|---|
| 20 | * redirect-endpoint, sturen hem daarheen
|
|---|
| 21 | * 2. zijn server controleert hem -> en vraagt ONS om een token
|
|---|
| 22 | * 3. wij verifieren die ondertekende -> token terug, versleuteld met ZIJN
|
|---|
| 23 | * aanvraag publieke sleutel
|
|---|
| 24 | * 4. zijn server ontsleutelt -> stuurt hem terug met ?owt=<token>
|
|---|
| 25 | * 5. wij wisselen het token in -> nu weten we wie hij is
|
|---|
| 26 | *
|
|---|
| 27 | * DRIE DINGEN DIE DE FEP ALS AANVAL BESCHRIJFT, en die hieronder staan omdat ze
|
|---|
| 28 | * anders precies de fout worden die je niet ziet:
|
|---|
| 29 | *
|
|---|
| 30 | * - IMPERSONATIE. `?zid=` mag NOOIT iemands identiteit bepalen; alleen het
|
|---|
| 31 | * ingewisselde `?owt=` telt. Mallory kan een link maken met zid=bob@elders,
|
|---|
| 32 | * en komt dan terug met een token dat MALLORY zegt. Wie zid gelooft, laat
|
|---|
| 33 | * Mallory als Bob binnen.
|
|---|
| 34 | * - OPEN REDIRECT. Het redirect-endpoint dat we uit webfinger halen moet
|
|---|
| 35 | * dezelfde host hebben als het adres dat de bezoeker intypte, anders sturen
|
|---|
| 36 | * wij bezoekers naar waar een vreemde maar wil.
|
|---|
| 37 | * - DoS. Tokens vervallen in minuten en gaan na een keer gebruiken weg.
|
|---|
| 38 | */
|
|---|
| 39 | import crypto from 'crypto';
|
|---|
| 40 | import db from '../config/database.js';
|
|---|
| 41 |
|
|---|
| 42 | /** Kort, want tussen stap 3 en 5 zit alleen een redirect. De FEP noemt "a couple of minutes". */
|
|---|
| 43 | export const TOKEN_TTL_MS = 3 * 60 * 1000;
|
|---|
| 44 |
|
|---|
| 45 | /** rel-waarden uit de FEP. Letterlijk, want hier hangt de vindbaarheid aan. */
|
|---|
| 46 | export const REL_TOKEN = 'http://purl.org/openwebauth/v1';
|
|---|
| 47 | export const REL_REDIRECT = 'http://purl.org/openwebauth/v1#redirect';
|
|---|
| 48 |
|
|---|
| 49 | // ── tokens ────────────────────────────────────────────────────────────────
|
|---|
| 50 |
|
|---|
| 51 | /** Alles wat over tijd is weg. Draait bij elke uitgifte en elke inwisseling. */
|
|---|
| 52 | export function sweepTokens(now = Date.now()) {
|
|---|
| 53 | db.prepare('DELETE FROM owa_tokens WHERE created_at < ?').run(now - TOKEN_TTL_MS);
|
|---|
| 54 | }
|
|---|
| 55 |
|
|---|
| 56 | /**
|
|---|
| 57 | * Stap 3: een token voor deze actor, opgeslagen zodat we hem straks herkennen.
|
|---|
| 58 | * URL-veilig, want hij reist als query-parameter terug.
|
|---|
| 59 | */
|
|---|
| 60 | export function issueToken(actorUri, now = Date.now()) {
|
|---|
| 61 | sweepTokens(now);
|
|---|
| 62 | const token = crypto.randomBytes(32).toString('base64url');
|
|---|
| 63 | db.prepare('INSERT INTO owa_tokens (token, actor_uri, created_at) VALUES (?,?,?)')
|
|---|
| 64 | .run(token, String(actorUri), now);
|
|---|
| 65 | return token;
|
|---|
| 66 | }
|
|---|
| 67 |
|
|---|
| 68 | /**
|
|---|
| 69 | * Stap 5: eenmalig inwisselen. Geeft de actor terug, of null.
|
|---|
| 70 | *
|
|---|
| 71 | * Het verwijderen gebeurt ALTIJD, ook als het token te oud bleek: een token dat
|
|---|
| 72 | * eenmaal is aangeboden mag nooit een tweede kans krijgen.
|
|---|
| 73 | */
|
|---|
| 74 | export function redeemToken(token, now = Date.now()) {
|
|---|
| 75 | const t = String(token || '');
|
|---|
| 76 | if (!t) return null;
|
|---|
| 77 | const row = db.prepare('SELECT actor_uri, created_at FROM owa_tokens WHERE token = ?').get(t);
|
|---|
| 78 | if (row) db.prepare('DELETE FROM owa_tokens WHERE token = ?').run(t);
|
|---|
| 79 | sweepTokens(now);
|
|---|
| 80 | if (!row) return null;
|
|---|
| 81 | if (now - row.created_at > TOKEN_TTL_MS) return null;
|
|---|
| 82 | return row.actor_uri;
|
|---|
| 83 | }
|
|---|
| 84 |
|
|---|
| 85 | /**
|
|---|
| 86 | * Het token versleuteld met de PUBLIEKE sleutel van de actor, zodat alleen zijn
|
|---|
| 87 | * server het kan lezen. PKCS#1 v1.5 en base64url zonder '=' staan zo in de FEP;
|
|---|
| 88 | * dat is geen smaak maar interop met Hubzilla en (streams).
|
|---|
| 89 | */
|
|---|
| 90 | export function encryptTokenFor(token, publicKeyPem) {
|
|---|
| 91 | const buf = crypto.publicEncrypt(
|
|---|
| 92 | { key: publicKeyPem, padding: crypto.constants.RSA_PKCS1_PADDING },
|
|---|
| 93 | Buffer.from(String(token), 'utf8'),
|
|---|
| 94 | );
|
|---|
| 95 | return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | // ── ontdekken waar de bezoeker vandaan komt ───────────────────────────────
|
|---|
| 99 |
|
|---|
| 100 | /** `@iemand@ergens.nl`, `iemand@ergens.nl`, `acct:iemand@ergens.nl` -> {user, host}. */
|
|---|
| 101 | export function parseHandle(input) {
|
|---|
| 102 | const m = String(input || '').trim().replace(/^acct:/i, '').replace(/^@/, '')
|
|---|
| 103 | .match(/^([^@\s/]+)@([^@\s/]+)$/);
|
|---|
| 104 | if (!m) return null;
|
|---|
| 105 | const host = m[2].toLowerCase();
|
|---|
| 106 | if (!/^[a-z0-9.-]+(:\d+)?$/i.test(host)) return null;
|
|---|
| 107 | return { user: m[1], host, acct: `${m[1]}@${host}` };
|
|---|
| 108 | }
|
|---|
| 109 |
|
|---|
| 110 | /**
|
|---|
| 111 | * Stap 1: waar stuurt deze bezoeker zich heen om zich te bewijzen?
|
|---|
| 112 | *
|
|---|
| 113 | * De FEP: nieuwe implementaties horen te webfingeren, oude hard-coden /magic.
|
|---|
| 114 | * We doen het eerste en vallen terug op het tweede -- die terugval is veilig
|
|---|
| 115 | * omdat hij per constructie op DEZELFDE host ligt.
|
|---|
| 116 | *
|
|---|
| 117 | * En hier staat de open-redirect-controle: wat webfinger ook teruggeeft, het
|
|---|
| 118 | * moet de host zijn van het adres dat de bezoeker zelf intypte. Zonder die
|
|---|
| 119 | * regel wordt dit formulier een doorgeefluik naar elke gewenste URL.
|
|---|
| 120 | */
|
|---|
| 121 | export async function discoverRedirectEndpoint(handle, { fetchImpl = fetch } = {}) {
|
|---|
| 122 | const h = parseHandle(handle);
|
|---|
| 123 | if (!h) return null;
|
|---|
| 124 | const url = `https://${h.host}/.well-known/webfinger?resource=${encodeURIComponent('acct:' + h.acct)}`;
|
|---|
| 125 | let href = null;
|
|---|
| 126 | try {
|
|---|
| 127 | const r = await fetchImpl(url, { headers: { accept: 'application/jrd+json, application/json' } });
|
|---|
| 128 | if (r.ok) {
|
|---|
| 129 | const jrd = await r.json();
|
|---|
| 130 | const link = (jrd.links || []).find((l) => l && l.rel === REL_REDIRECT && l.href);
|
|---|
| 131 | if (link) href = link.href;
|
|---|
| 132 | }
|
|---|
| 133 | } catch { /* geen webfinger: hieronder de terugval */ }
|
|---|
| 134 | if (!href) href = `https://${h.host}/magic`;
|
|---|
| 135 | try {
|
|---|
| 136 | if (new URL(href).host.toLowerCase() !== h.host) return null; // open redirect
|
|---|
| 137 | } catch { return null; }
|
|---|
| 138 | return { endpoint: href, handle: h };
|
|---|
| 139 | }
|
|---|
| 140 |
|
|---|
| 141 | /** `bdest`: de terugkeer-URL als hex, zo staat het in de FEP. */
|
|---|
| 142 | export function toBdest(url) {
|
|---|
| 143 | return Buffer.from(String(url), 'utf8').toString('hex');
|
|---|
| 144 | }
|
|---|
| 145 |
|
|---|
| 146 | /**
|
|---|
| 147 | * De URL waar we de bezoeker heen sturen.
|
|---|
| 148 | *
|
|---|
| 149 | * De terugkeer-URL moet BINNEN onze eigen origin liggen -- en het liefst binnen
|
|---|
| 150 | * de PWA-scope (siteUrlBase), anders komt iemand die de site op zijn
|
|---|
| 151 | * beginscherm heeft na het inloggen terecht in een losse browsertab terwijl de
|
|---|
| 152 | * app uitgelogd blijft. Dat ziet eruit als "inloggen werkt niet" en is het niet.
|
|---|
| 153 | */
|
|---|
| 154 | export function buildRedirect(endpoint, returnUrl) {
|
|---|
| 155 | const u = new URL(endpoint);
|
|---|
| 156 | u.searchParams.set('owa', '1');
|
|---|
| 157 | u.searchParams.set('bdest', toBdest(returnUrl));
|
|---|
| 158 | return u.toString();
|
|---|
| 159 | }
|
|---|
| 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 | /**
|
|---|
| 236 | * Een deterministische nep-uitkomst, afgeleid uit de ciphertext en onze eigen
|
|---|
| 237 | * sleutel. Dit is de kern van implicit rejection: bij ongeldige padding geven we
|
|---|
| 238 | * GEEN fout maar een waarde, zodat "klopte de padding" nergens af te lezen is.
|
|---|
| 239 | *
|
|---|
| 240 | * DETERMINISTISCH, en dat is geen detail. Zou dit verse willekeur zijn, dan
|
|---|
| 241 | * geeft dezelfde ciphertext twee keer aanbieden twee verschillende antwoorden --
|
|---|
| 242 | * en juist dat verschil is het onderscheid dat we wilden verbergen. Zo doen TLS
|
|---|
| 243 | * en OpenSSL 3.2 het ook: afgeleid uit sleutel + ciphertext, dus stabiel bij
|
|---|
| 244 | * herhaling en onvoorspelbaar voor wie de sleutel niet heeft.
|
|---|
| 245 | *
|
|---|
| 246 | * Geëxporteerd omdat die eigenschap toetsbaar moet zijn; buiten de tests heeft
|
|---|
| 247 | * niemand hem nodig.
|
|---|
| 248 | *
|
|---|
| 249 | * EERLIJK OVER WAT DIT WEL EN NIET DRAAGT (gemeten 19-8): haal je hem weg, dan
|
|---|
| 250 | * blijft de suite groen. De andere tak geeft dan een LEGE string terug, en die
|
|---|
| 251 | * sneuvelt net zo goed op de tekenset-controle hieronder -- "werpt niet" en
|
|---|
| 252 | * "levert geen token" zijn dus al gedekt zonder deze functie. Wat hij toevoegt
|
|---|
| 253 | * is dat ALLE faalwegen dezelfde vorm teruggeven: verkeerde sleutel, verkeerde
|
|---|
| 254 | * lengte, kapotte base64, ongeldige padding. Een lege string is een verklikker
|
|---|
| 255 | * voor wie ooit naar de rauwe waarde kijkt in plaats van naar het eindoordeel;
|
|---|
| 256 | * afgeleide bytes zijn dat niet. Zo doen TLS en OpenSSL 3.2 het ook.
|
|---|
| 257 | */
|
|---|
| 258 | export function _nepUitkomst(privatePem, ct) {
|
|---|
| 259 | const geheim = crypto.createHash('sha256').update(String(privatePem)).digest();
|
|---|
| 260 | return crypto.createHmac('sha256', geheim).update(ct).digest().toString('latin1');
|
|---|
| 261 | }
|
|---|
| 262 |
|
|---|
| 263 | /**
|
|---|
| 264 | * PKCS#1 v1.5 zelf uitpakken (EME-PKCS1-v1_5: 00 02 PS 00 M).
|
|---|
| 265 | *
|
|---|
| 266 | * WAAROM ZELF: Node weigert `privateDecrypt` met RSA_PKCS1_PADDING sinds de
|
|---|
| 267 | * mitigatie voor CVE-2023-46809 (Marvin). De revert-vlag bestaat alleen op de
|
|---|
| 268 | * lijnen 18/20/21 -- Node 22+ heeft hem nooit gehad, en 20 is sinds 30 april
|
|---|
| 269 | * 2026 EOL. Er is dus geen weg terug; zie shaer-r15.
|
|---|
| 270 | *
|
|---|
| 271 | * OpenWebAuth (FEP-61cf) schrijft v1.5 voor, dus overstappen op OAEP repareert
|
|---|
| 272 | * de fout en breekt de interop met Hubzilla. Blijft over: `RSA_NO_PADDING` en
|
|---|
| 273 | * het omhulsel er zelf afhalen -- precies het stuk dat de CVE veroorzaakte, dus
|
|---|
| 274 | * met de zorg die daarbij hoort.
|
|---|
| 275 | *
|
|---|
| 276 | * GEEN VROEGE UITGANG EN GEEN WORP. De scan loopt altijd het hele blok af en
|
|---|
| 277 | * beide takken doen hetzelfde werk. Dat is geen echte constant-time -- die
|
|---|
| 278 | * krijg je in JavaScript met JIT en GC niet -- maar het haalt wel het
|
|---|
| 279 | * waarneembare verschil weg. Wat de aanval hier echt begrenst is de teller op
|
|---|
| 280 | * /magic: een orakel heeft honderdduizenden pogingen nodig.
|
|---|
| 281 | */
|
|---|
| 282 | function pakUit(blok, privatePem, ct) {
|
|---|
| 283 | const k = blok.length;
|
|---|
| 284 | // Kop: 00 02. Als getal uitrekenen, niet als vertakking.
|
|---|
| 285 | let goed = ((blok[0] === 0x00) & (blok[1] === 0x02));
|
|---|
| 286 | // Eerste nulbyte vanaf 2 zoeken ZONDER de lus te verlaten.
|
|---|
| 287 | let sep = -1;
|
|---|
| 288 | for (let i = 2; i < k; i++) {
|
|---|
| 289 | const isNul = blok[i] === 0x00 ? 1 : 0;
|
|---|
| 290 | const nogNiet = sep === -1 ? 1 : 0;
|
|---|
| 291 | sep = sep + (isNul & nogNiet) * (i - sep);
|
|---|
| 292 | }
|
|---|
| 293 | // PS moet minstens 8 bytes zijn (RFC 8017), dus de scheider ligt op >= 10.
|
|---|
| 294 | goed = goed & (sep >= 10 ? 1 : 0) & (sep < k ? 1 : 0);
|
|---|
| 295 | const echt = blok.subarray(goed ? sep + 1 : k).toString('utf8');
|
|---|
| 296 | const nep = _nepUitkomst(privatePem, ct);
|
|---|
| 297 | return goed ? echt : nep;
|
|---|
| 298 | }
|
|---|
| 299 |
|
|---|
| 300 | /** Het token uitpakken met onze eigen prive-sleutel. */
|
|---|
| 301 | export function decryptToken(encrypted, privatePem) {
|
|---|
| 302 | const b64 = String(encrypted || '').replace(/-/g, '+').replace(/_/g, '/');
|
|---|
| 303 | const ct = Buffer.from(b64, 'base64');
|
|---|
| 304 | let k = 0;
|
|---|
| 305 | try { k = crypto.createPublicKey(privatePem).asymmetricKeyDetails.modulusLength / 8; } catch { k = 0; }
|
|---|
| 306 |
|
|---|
| 307 | // Een blok van de verkeerde lengte zegt niets over de sleutel, maar het zou
|
|---|
| 308 | // wel werpen -- en een worp is precies het signaal dat we kwijt willen. Dus
|
|---|
| 309 | // dezelfde weg als een ongeldige padding.
|
|---|
| 310 | let blok = null;
|
|---|
| 311 | if (k && ct.length === k) {
|
|---|
| 312 | try {
|
|---|
| 313 | blok = crypto.privateDecrypt({ key: privatePem, padding: crypto.constants.RSA_NO_PADDING }, ct);
|
|---|
| 314 | } catch { blok = null; }
|
|---|
| 315 | }
|
|---|
| 316 | const t = (blok && blok.length === k) ? pakUit(blok, privatePem, ct) : _nepUitkomst(privatePem, ct);
|
|---|
| 317 |
|
|---|
| 318 | // Een token is URL-veilige tekst. Wat hierboven uit een mislukking komt is
|
|---|
| 319 | // afgeleide onzin, en die hoort hier te stranden in plaats van als token de
|
|---|
| 320 | // wereld in te gaan.
|
|---|
| 321 | //
|
|---|
| 322 | // ALLEBEI de voorwaarden doen werk, en dat is gemeten met 300 vreemde sleutels:
|
|---|
| 323 | // - de TEKENSET vangt vrijwel alles. Van die 300 was er geen enkele die
|
|---|
| 324 | // volledig uit URL-veilige tekens bestond.
|
|---|
| 325 | // - de ONDERGRENS vangt de rest. De onzin heeft een willekeurige lengte, en
|
|---|
| 326 | // bij een kort stukje is "toevallig allemaal URL-veilig" niet meer
|
|---|
| 327 | // verwaarloosbaar: per byte is die kans ruwweg een kwart.
|
|---|
| 328 | // Zestien is daarmee geen rond getal maar een grens die iets doet. Een echte
|
|---|
| 329 | // implementatie zit er ruim boven (de onze: 43 tekens).
|
|---|
| 330 | return /^[A-Za-z0-9._~-]{16,512}$/.test(t) ? t : null;
|
|---|
| 331 | }
|
|---|
| 332 |
|
|---|
| 333 | // ── wie is er binnen ──────────────────────────────────────────────────────
|
|---|
| 334 |
|
|---|
| 335 | /** De actor die deze sessie bewees te zijn, of null. */
|
|---|
| 336 | export function guestActor(req) {
|
|---|
| 337 | const g = req && req.session && req.session.owa;
|
|---|
| 338 | return (g && typeof g.actor === 'string' && g.actor) ? g.actor : null;
|
|---|
| 339 | }
|
|---|
| 340 |
|
|---|
| 341 | /** Volgt deze actor deze site? Dat is de vraag die `fan_only` altijd al stelde. */
|
|---|
| 342 | export function isFollowerOf(slug, actorUri) {
|
|---|
| 343 | if (!slug || !actorUri) return false;
|
|---|
| 344 | const row = db.prepare('SELECT 1 FROM ap_followers WHERE slug = ? AND actor_uri = ? LIMIT 1')
|
|---|
| 345 | .get(String(slug), String(actorUri));
|
|---|
| 346 | return !!row;
|
|---|
| 347 | }
|
|---|
| 348 |
|
|---|
| 349 | /**
|
|---|
| 350 | * Alles wat een poort over deze bezoeker moet weten, op één plek.
|
|---|
| 351 | *
|
|---|
| 352 | * Bewust hier en niet in PostAccessService: die module beslist en raakt de
|
|---|
| 353 | * database niet aan. Deze haalt op, die beslist.
|
|---|
| 354 | */
|
|---|
| 355 | export function viewerFor(req, site, extra = {}) {
|
|---|
| 356 | const actor = guestActor(req);
|
|---|
| 357 | return {
|
|---|
| 358 | user: (req && req.session && req.session.user) || null,
|
|---|
| 359 | site: site || null,
|
|---|
| 360 | fediActor: actor,
|
|---|
| 361 | isFollower: actor && site ? isFollowerOf(site.slug, actor) : false,
|
|---|
| 362 | ...extra,
|
|---|
| 363 | };
|
|---|
| 364 | }
|
|---|
| 365 |
|
|---|
| 366 | export default {
|
|---|
| 367 | TOKEN_TTL_MS, REL_TOKEN, REL_REDIRECT,
|
|---|
| 368 | sweepTokens, issueToken, redeemToken, encryptTokenFor,
|
|---|
| 369 | parseHandle, discoverRedirectEndpoint, toBdest, buildRedirect,
|
|---|
| 370 | guestActor, isFollowerOf, viewerFor,
|
|---|
| 371 | fromBdest, discoverTokenEndpoint, requestToken, decryptToken,
|
|---|
| 372 | };
|
|---|