-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdemo-multiparty.js
More file actions
328 lines (253 loc) · 12.2 KB
/
Copy pathdemo-multiparty.js
File metadata and controls
328 lines (253 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/*
HWT Demo: Multi-Party Authorization
deno run -A ./demo-multiparty.js
What this demonstrates:
Two independent organizations issue approver tokens to their own principals.
A coordinator service verifies both tokens against their respective issuers'
JWKS (spec §6, §12) — no shared identity provider, no prior agreement between
the organizations. Only when both verify does the coordinator issue a joint
authorization token using a private authz schema (spec §4.2) that records the
satisfied quorum. Any downstream service can verify the coordinator token and
inspect the approver identities from the token alone.
SCENARIO:
1. Organization A (hostA) issues an approver token to user:alice
2. Organization B (hostB) issues an approver token to user:bob
3. Coordinator (hostC) verifies both tokens cross-origin (spec §12)
4. Quorum satisfied → coordinator issues joint authorization token (private schema)
5. Downstream verifies the coordinator token — no prior knowledge of hostC
6. alice's root token revoked at hostA → coordinator token unaffected (spec §13)
*/
import { ensureServers, hostA, hostB, hostC, get, post, checkInstance } from './demo_hosts.js';
await ensureServers();
function tokenPreview(token){
return token.length > 80 ? token.slice(0, 40) + '…' + token.slice(-20) : token;
}
// ── main ──────────────────────────────────────────────────────────────────────
async function main(){
console.log(`
HWT Demo: Multi-Party Authorization
── prerequisites ──────────────────────────────────────────────────────────
0) Checking instances
`);
const infoA = await checkInstance(hostA, 'Organization A (hostA)');
const infoB = await checkInstance(hostB, 'Organization B (hostB)');
const infoC = await checkInstance(hostC, 'Coordinator (hostC)');
console.log(`
Three independent services — independent key pairs, no shared secrets.
hostA kid: ${infoA.kid}
hostB kid: ${infoB.kid}
hostC kid: ${infoC.kid}
── step 1: ──
HWT: any token is verifiable against its issuer's published JWKS (spec §12).
A coordinator can verify N tokens from N independent issuers, then issue
its own signed attestation that the quorum was satisfied. Every layer is
independently verifiable from the token alone — alice's token against
hostA's JWKS, bob's against hostB's, the joint token against hostC's.
No central service. No prior registration between the three.
`);
// ── step 2: organization A issues approver token to alice ─────────────────
console.log(`
── step 2: organization A (hostA) issues approver token to user:alice ─────
2) Issuer: hostA subject: user:alice authz: RBAC/1.0.2 roles: [approver]
`);
const { token: aliceToken, tid: aliceTid } = await post(`${hostA}/api/token`, {
payload: {
iss: hostA,
sub: 'user:alice',
authz: { scheme: 'RBAC/1.0.2', roles: ['approver'] }
},
expiresInSeconds: 3600
});
console.log(`
Alice's token — issued by hostA
token: ${tokenPreview(aliceToken)}
tid: ${aliceTid}
iss: ${hostA}
sub: user:alice
authz: { scheme: 'RBAC/1.0.2', roles: ['approver'] }
del: [] — root token, no delegation history
`);
// ── step 3: organization B issues approver token to bob ───────────────────
console.log(`
── step 3: organization B (hostB) issues approver token to user:bob ───────
3) Issuer: hostB subject: user:bob authz: RBAC/1.0.2 roles: [approver]
`);
const { token: bobToken, tid: bobTid } = await post(`${hostB}/api/token`, {
payload: {
iss: hostB,
sub: 'user:bob',
authz: { scheme: 'RBAC/1.0.2', roles: ['approver'] }
},
expiresInSeconds: 3600
});
console.log(`
Bob's token — issued by hostB
token: ${tokenPreview(bobToken)}
tid: ${bobTid}
iss: ${hostB}
sub: user:bob
authz: { scheme: 'RBAC/1.0.2', roles: ['approver'] }
del: [] — root token, no delegation history
`);
// ── step 4: coordinator verifies alice's token cross-origin ───────────────
console.log(`
── step 4: coordinator (hostC) verifies alice's token — no prior knowledge of hostA ──
4) hostC calls /api/verify-external (spec §12 verification path)
Fetches: ${hostA}/.well-known/hwt-keys.json (spec §6 key discovery)
Verifies signature locally against fetched keys — no further network call
`);
const aliceVerify = await post(`${hostC}/api/verify-external`, { token: aliceToken });
if(!aliceVerify.ok){
throw new Error(`alice's token failed verification: ${aliceVerify.error}`);
}
console.log(`
Signature valid — alice's token verified by hostC
verified sub: ${aliceVerify.data.sub}
verified authz: ${ JSON.stringify(aliceVerify.data.authz) }
verified iss: ${aliceVerify.data.iss}
JWKS fetched: ${aliceVerify._external?.jwksUrl}
Not expired · Signature valid
`);
// ── step 5: coordinator verifies bob's token cross-origin ─────────────────
console.log(`
── step 5: coordinator (hostC) verifies bob's token — no prior knowledge of hostB ──
5) hostC calls /api/verify-external (spec §12 verification path)
Fetches: ${hostB}/.well-known/hwt-keys.json (spec §6 key discovery)
`);
const bobVerify = await post(`${hostC}/api/verify-external`, { token: bobToken });
if(!bobVerify.ok){
throw new Error(`bob's token failed verification: ${bobVerify.error}`);
}
console.log(`
Signature valid — bob's token verified by hostC
verified sub: ${bobVerify.data.sub}
verified authz: ${ JSON.stringify(bobVerify.data.authz) }
verified iss: ${bobVerify.data.iss}
JWKS fetched: ${bobVerify._external?.jwksUrl}
Not expired · Signature valid
`);
// ── step 6: quorum satisfied — coordinator issues joint auth token ─────────
console.log(`
── step 6: quorum satisfied — coordinator issues joint authorization token ─
6) Both principals verified. hostC issues a joint auth token.
authz uses a private schema (spec §4.2). Origin-relative scheme path
is resolved against iss: ${hostC}/schemas/joint-approval/v1
Private schemas require no registration (spec §4.4). The URL form is
the unambiguous signal to any verifier that this is a private schema.
The quorum record goes in authz — not del[]. del[] is a linear delegation
chain tracing authorization lineage hop by hop. It has no multi-parent
form. The coordinator is not delegating from either alice or bob; it is
issuing its own signed attestation that the quorum was satisfied.
`);
const jointAuthz = {
scheme: '/schemas/joint-approval/v1',
action: 'release:contract-7f2a',
quorum: '2-of-2',
approvers: [
{ iss: aliceVerify.data.iss, sub: aliceVerify.data.sub },
{ iss: bobVerify.data.iss, sub: bobVerify.data.sub }
]
};
const { token: jointToken, tid: jointTid } = await post(`${hostC}/api/token`, {
payload: {
iss: hostC,
sub: 'svc:coordinator',
authz: jointAuthz
},
expiresInSeconds: 3600
});
console.log(`
Joint authorization token — issued by hostC
token: ${tokenPreview(jointToken)}
tid: ${jointTid}
iss: ${hostC}
sub: svc:coordinator
authz: ${ JSON.stringify(jointAuthz) }
↑ private schema — approver identities embedded in the token
del: [] — coordinator root, not a delegation chain
`);
// ── step 7: downstream verifies the coordinator token cross-origin ─────────
console.log(`
── step 7: downstream verifies coordinator token — no prior knowledge of hostC ──
7) A downstream service (simulated here via hostA's /api/verify-external)
Fetches: ${hostC}/.well-known/hwt-keys.json (spec §6 key discovery)
Verifies signature per spec §12
The downstream inspects authz.approvers directly in the verified payload.
hostA and hostB are not contacted at this step — the coordinator already
did that work and signed the result into the token.
`);
const jointVerify = await post(`${hostA}/api/verify-external`, { token: jointToken });
if(!jointVerify.ok){
throw new Error(`joint auth token failed verification: ${jointVerify.error}`);
}
console.log(`
Joint auth token verified by downstream
verified sub: ${jointVerify.data.sub}
verified iss: ${jointVerify.data.iss}
verified authz: ${ JSON.stringify(jointVerify.data.authz) }
JWKS fetched: ${jointVerify._external?.jwksUrl}
approvers confirmed — no contact with hostA or hostB required
Not expired · Signature valid
`);
// ── step 8: alice's root token revoked — coordinator token unaffected ──────
console.log(`
── step 8: alice's root token revoked — coordinator token unaffected ───────
8) Simulates: alice's access withdrawn after the joint approval was issued.
Revocation is application-layer behavior — explicitly outside the HWT
protocol scope (spec §13). The library provides it as an opt-in extension
via endpoints.revocation in /.well-known/hwt.json.
`);
await post(`${hostA}/api/revoke`, { tid: aliceTid });
console.log(`
Alice's root token revoked on hostA (tid: ${aliceTid})
`);
// Alice's root token is now rejected at hostA (local revocation check)
const aliceRevokedCheck = await post(`${hostA}/api/verify`, { token: aliceToken })
.catch(res => res.data);
console.log(`
Alice's root token at hostA: ${ aliceRevokedCheck?.ok
? '(still valid — revocation state may not have propagated)'
: `correctly rejected — ${ aliceRevokedCheck?.error }` }
`);
// The coordinator token is a separate root issued by hostC.
// Revoking alice's token at hostA has no reach into hostC's token store.
const jointStillValid = await post(`${hostA}/api/verify-external`, { token: jointToken })
.catch(res => res.data);
console.log(`
Joint auth token (tid: ${jointTid}, iss: hostC):
${ jointStillValid?.ok
? `still valid — it is a root token signed by hostC, independent of alice's token at hostA`
: `unexpected failure: ${ jointStillValid?.error }` }
The coordinator token's signature covers the payload at issuance. Revoking
alice's underlying token at hostA does not propagate to hostC's token store.
To invalidate this joint auth token, the downstream revokes tid ${jointTid}
at hostC — not alice's token at hostA. Each token's state is managed at
its own issuer. This is deliberate: token state is outside the protocol
(spec §13). Short token lifetimes are the primary bounding mechanism;
application-layer revocation adds finer control when needed.
`);
// ── summary ────────────────────────────────────────────────────────────────
console.log(`
── summary ────────────────────────────────────────────────────────────────
Three independent issuers — zero pre-coordination:
user:alice @ hostA verified by hostC against ${hostA}/.well-known/hwt-keys.json
user:bob @ hostB verified by hostC against ${hostB}/.well-known/hwt-keys.json
svc:coordinator @ hostC verified by downstream against ${hostC}/.well-known/hwt-keys.json
Each verification: fetch issuer JWKS (spec §6) → verify signature locally
(spec §12). No central IdP. No federation agreement. No shared secrets.
The joint auth token carries a private authz schema (spec §4.2) embedding
both approver identities. Any downstream can inspect the quorum record
from the token alone — without re-contacting hostA or hostB.
authz structure note:
del[] is a linear delegation chain — it has no multi-parent form and
does not apply here. The coordinator is not delegating from alice or bob;
it is issuing a new signed attestation. The approver identities are
application data carried in authz, not protocol delegation records.
`);
Deno.exit();
}
main().catch(error => {
console.warn(`Error:`, error.message);
console.error(error);
Deno.exit(1);
});