| [af2cc73] | 1 | /**
|
|---|
| 2 | * OpenWebAuth (FEP-61cf), de drie plekken waar de stroom ons raakt.
|
|---|
| 3 | *
|
|---|
| 4 | * POST /owa/login de bezoeker geeft zijn adres, wij sturen hem naar huis
|
|---|
| 5 | * ALL /owa/token zijn server vraagt ondertekend om een token (stap 2/3)
|
|---|
| 6 | * GET /owa/logout weer anoniem
|
|---|
| 7 | *
|
|---|
| 8 | * Plus de middleware onderaan, die op ELK verzoek naar `?owt=` en `?zid=` kijkt.
|
|---|
| 9 | *
|
|---|
| 10 | * Zie OpenWebAuthService voor de stroom als geheel en voor de drie aanvallen
|
|---|
| 11 | * die de FEP beschrijft.
|
|---|
| 12 | */
|
|---|
| 13 | import express from 'express';
|
|---|
| 14 | import * as AP from '../services/ActivityPubService.js';
|
|---|
| 15 | import * as OWA from '../services/OpenWebAuthService.js';
|
|---|
| 16 |
|
|---|
| 17 | const router = express.Router();
|
|---|
| 18 |
|
|---|
| 19 | /** Waar de bezoeker weer uitkomt. Altijd binnen onze eigen origin. */
|
|---|
| 20 | function returnUrlFor(req, path) {
|
|---|
| 21 | const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
|
|---|
| 22 | // Alleen een pad, nooit iets wat de bezoeker meegaf als volledige URL: dat is
|
|---|
| 23 | // hoe een aanmeldformulier een open redirect wordt.
|
|---|
| 24 | let p = String(path || '/');
|
|---|
| 25 | if (!p.startsWith('/') || p.startsWith('//')) p = '/';
|
|---|
| 26 | return base + p;
|
|---|
| 27 | }
|
|---|
| 28 |
|
|---|
| 29 | /**
|
|---|
| 30 | * Stap 2/3: de home instance vraagt ondertekend om een token.
|
|---|
| 31 | *
|
|---|
| 32 | * GET EN POST, want de FEP zegt dat sommige home instances een POST met een
|
|---|
| 33 | * willekeurig lijf sturen.
|
|---|
| 34 | *
|
|---|
| 35 | * De verificatie is niet nieuw geschreven: AP.verifyRequest() doet dit al voor
|
|---|
| 36 | * de inbox, inclusief het vastpinnen van de sleutel op de herkomst van de actor
|
|---|
| 37 | * (anders host je een document met andermans id naast je eigen sleutel), een
|
|---|
| 38 | * replay-venster op Date, en een verplichte digest zodra er een lijf is. Eén
|
|---|
| 39 | * implementatie van "is deze aanvraag echt van wie hij zegt".
|
|---|
| 40 | */
|
|---|
| 41 | router.all('/owa/token', async (req, res) => {
|
|---|
| 42 | let verified = null;
|
|---|
| 43 | try { verified = await AP.verifyRequest(req); } catch { verified = null; }
|
|---|
| 44 | if (!verified || !verified.id) {
|
|---|
| 45 | return res.status(401).json({ success: false });
|
|---|
| 46 | }
|
|---|
| 47 | const pem = verified.publicKey && verified.publicKey.publicKeyPem;
|
|---|
| 48 | if (!pem) return res.status(400).json({ success: false });
|
|---|
| 49 |
|
|---|
| 50 | try {
|
|---|
| 51 | const token = OWA.issueToken(verified.id);
|
|---|
| 52 | // Versleuteld met ZIJN publieke sleutel: alleen de server die de bijbehorende
|
|---|
| 53 | // prive-sleutel heeft kan hem lezen. Daarmee bewijst de teruggave dat de
|
|---|
| 54 | // ondertekenaar ook echt die actor beheert, en niet alleen zijn document kent.
|
|---|
| 55 | return res.json({ success: true, encrypted_token: OWA.encryptTokenFor(token, pem) });
|
|---|
| 56 | } catch (e) {
|
|---|
| 57 | console.warn('[owa] token uitgeven mislukte:', e && e.message);
|
|---|
| 58 | return res.status(500).json({ success: false });
|
|---|
| 59 | }
|
|---|
| 60 | });
|
|---|
| 61 |
|
|---|
| 62 | /**
|
|---|
| 63 | * Stap 1: de bezoeker typt zijn adres en wij sturen hem naar zijn eigen server.
|
|---|
| 64 | *
|
|---|
| 65 | * We slaan hier NIETS op over wie hij zegt te zijn. Dat is opzet: pas het
|
|---|
| 66 | * ingewisselde token bepaalt de identiteit (zie de impersonatie-aanval in de
|
|---|
| 67 | * FEP). Een `zid`/handle is niet meer dan een routeringshint.
|
|---|
| 68 | */
|
|---|
| 69 | router.post('/owa/login', async (req, res) => {
|
|---|
| 70 | const terug = returnUrlFor(req, req.body && req.body.next);
|
|---|
| 71 | const found = await OWA.discoverRedirectEndpoint(req.body && req.body.handle);
|
|---|
| 72 | if (!found) {
|
|---|
| 73 | const u = new URL(terug);
|
|---|
| 74 | u.searchParams.set('owa_error', '1');
|
|---|
| 75 | return res.redirect(u.toString());
|
|---|
| 76 | }
|
|---|
| 77 | return res.redirect(OWA.buildRedirect(found.endpoint, terug));
|
|---|
| 78 | });
|
|---|
| 79 |
|
|---|
| 80 | /** Weer anoniem. Raakt een eventuele lokale sessie niet aan. */
|
|---|
| 81 | router.get('/owa/logout', (req, res) => {
|
|---|
| 82 | if (req.session) delete req.session.owa;
|
|---|
| 83 | res.redirect(returnUrlFor(req, req.query && req.query.next));
|
|---|
| 84 | });
|
|---|
| 85 |
|
|---|
| 86 | /**
|
|---|
| 87 | * Op elk verzoek: is er een token ingewisseld, of wil iemand de stroom starten?
|
|---|
| 88 | *
|
|---|
| 89 | * `owt` BEPAALT de identiteit. `zid` start hooguit de stroom en wordt verder
|
|---|
| 90 | * genegeerd -- dat onderscheid IS de impersonatie-verdediging uit de FEP.
|
|---|
| 91 | */
|
|---|
| 92 | export function owaMiddleware(req, res, next) {
|
|---|
| 93 | if (req.method !== 'GET' || !req.query) return next();
|
|---|
| 94 |
|
|---|
| 95 | if (req.query.owt) {
|
|---|
| 96 | let actor = null;
|
|---|
| 97 | try { actor = OWA.redeemToken(req.query.owt); } catch { actor = null; }
|
|---|
| 98 | if (actor) req.session.owa = { actor, at: Date.now() };
|
|---|
| 99 | // Het token uit de URL halen: hij is toch al opgebruikt, en zo blijft hij
|
|---|
| 100 | // niet in de geschiedenis, de titelbalk of een gedeelde link staan.
|
|---|
| 101 | const u = new URL(req.originalUrl, process.env.PUBLIC_BASE_URL || 'https://localhost');
|
|---|
| 102 | u.searchParams.delete('owt');
|
|---|
| 103 | u.searchParams.delete('zid');
|
|---|
| 104 | return res.redirect(u.pathname + (u.search || ''));
|
|---|
| 105 | }
|
|---|
| 106 |
|
|---|
| 107 | if (req.query.zid && !OWA.guestActor(req)) {
|
|---|
| 108 | const u = new URL(req.originalUrl, process.env.PUBLIC_BASE_URL || 'https://localhost');
|
|---|
| 109 | u.searchParams.delete('zid');
|
|---|
| 110 | const terug = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '') + u.pathname + (u.search || '');
|
|---|
| 111 | // Geen await in een middleware-keten die verder synchroon is: bij een fout
|
|---|
| 112 | // gaat de bezoeker gewoon door naar de pagina, alleen zonder inlog.
|
|---|
| 113 | OWA.discoverRedirectEndpoint(req.query.zid)
|
|---|
| 114 | .then((found) => {
|
|---|
| 115 | if (found) return res.redirect(OWA.buildRedirect(found.endpoint, terug));
|
|---|
| 116 | return res.redirect(terug);
|
|---|
| 117 | })
|
|---|
| 118 | .catch(() => res.redirect(terug));
|
|---|
| 119 | return;
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | return next();
|
|---|
| 123 | }
|
|---|
| 124 |
|
|---|
| 125 | export default router;
|
|---|