Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/session.mjs

2.8 KiB, 1 run

created by r2519314175:131, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// session.mjs — sign a browser page in to the gateway as a fresh account.
2//
3// The operator console no longer takes a token: it rides the ordinary signed
4// session the app takes when you unlock your identity, so a test that wants to
5// reach the console has to hold a real one. Faking it — injecting a cookie,
6// stubbing the fetch — would test the console against a session the gateway
7// never issued, and the thing most worth testing here is precisely that the
8// gateway issues it and honours it.
9//
10// So this does what the app does, in the page, with WebCrypto: generate an
11// Ed25519 device key, bind it to an account with a signature over the same
12// message `gateway.js` signs, then answer a challenge to take the session. The
13// cookie lands in the browser's own jar because the browser is what asked for
14// it.
15
16/// Register a fresh account in `page`'s origin and take a session on it.
17///
18/// Returns the account id, which is what an owner needs in order to grant a
19/// role, and what the tests assert against.
20export async function signInFresh(page, appUrl) {
21 // The page must already be on the origin: the session cookie is bound to
22 // it, and a fetch from about:blank would have nowhere to put one.
23 if (!page.url().startsWith(appUrl)) {
24 await page.goto(appUrl + '/', { waitUntil: 'domcontentloaded' });
25 }
26 return await page.evaluate(async () => {
27 function b64(buf) {
28 var bytes = new Uint8Array(buf), s = '';
29 for (var i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
30 return btoa(s);
31 }
32 function b64url(buf) {
33 return b64(buf).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
34 }
35 async function post(path, body) {
36 var r = await fetch(path, {
37 method: 'POST',
38 credentials: 'same-origin',
39 headers: { 'content-type': 'application/json', 'x-daimond-api': '1' },
40 body: JSON.stringify(body),
41 });
42 var j = null;
43 try { j = await r.json(); } catch (e) {}
44 if (!r.ok || !j || j.ok === false) {
45 throw new Error(path + ' → ' + r.status + ' ' + ((j && j.error) || ''));
46 }
47 return j;
48 }
49
50 var kp = await crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']);
51 var raw = await crypto.subtle.exportKey('raw', kp.publicKey);
52 var pub = b64url(raw);
53 var enc = new TextEncoder();
54 var sign = async function (s) {
55 return b64(await crypto.subtle.sign({ name: 'Ed25519' }, kp.privateKey, enc.encode(s)));
56 };
57
58 var ts = Math.floor(Date.now() / 1000);
59 var acct = await post('/api/account', {
60 pubkey: pub, alg: 'Ed25519', ts: ts,
61 sig: await sign('daimond-gw-account:v1:' + pub + ':' + ts),
62 });
63 var ch = await post('/api/auth/challenge', { pubkey: pub, alg: 'Ed25519' });
64 await post('/api/auth/verify', {
65 challenge_id: ch.challenge_id,
66 sig: await sign(ch.challenge),
67 });
68 return acct.account_id;
69 });
70}