|
| 1 | +/* eslint-disable max-len */ |
| 2 | + |
| 3 | +import { fetch } from 'cross-fetch'; |
| 4 | +import { Parser, Store } from 'n3'; |
| 5 | + |
| 6 | +const terms = { |
| 7 | + solid: { |
| 8 | + umaServer: 'http://www.w3.org/ns/solid/terms#umaServer', |
| 9 | + viewIndex: 'http://www.w3.org/ns/solid/terms#viewIndex', |
| 10 | + entry: 'http://www.w3.org/ns/solid/terms#entry', |
| 11 | + filter: 'http://www.w3.org/ns/solid/terms#filter', |
| 12 | + location: 'http://www.w3.org/ns/solid/terms#location', |
| 13 | + }, |
| 14 | + filters: { |
| 15 | + bday: 'http://localhost:3000/catalog/public/filters/bday', |
| 16 | + age: 'http://localhost:3000/catalog/public/filters/age', |
| 17 | + } |
| 18 | +} |
| 19 | + |
| 20 | +const parser = new Parser(); |
| 21 | + |
| 22 | +const privateRequest = async (resource_id: string, tokenEndpoint: string) => { |
| 23 | + const claim_token = "http://localhost:3000/demo/public/bday-app" |
| 24 | + |
| 25 | + const content = { |
| 26 | + grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', |
| 27 | + claim_token: encodeURIComponent(claim_token), |
| 28 | + claim_token_format: 'urn:solidlab:uma:claims:formats:webid', |
| 29 | + // ticket, |
| 30 | + permissions: [{ |
| 31 | + resource_id, |
| 32 | + resource_scopes: [ 'urn:example:css:modes:read', 'urn:example:css:modes:write' ], |
| 33 | + }] |
| 34 | + }; |
| 35 | + |
| 36 | + const asRequestResponse = await fetch(tokenEndpoint, { |
| 37 | + method: "POST", |
| 38 | + headers: { |
| 39 | + "content-type":"application/json" |
| 40 | + }, |
| 41 | + body: JSON.stringify(content), |
| 42 | + }); |
| 43 | + |
| 44 | + const asResponse = await asRequestResponse.json(); |
| 45 | + const tokenResponse = await fetch(resource_id, { |
| 46 | + headers: { 'Authorization': `${asResponse.token_type} ${asResponse.access_token}` } |
| 47 | + }); |
| 48 | +} |
| 49 | + |
| 50 | +const log = (msg: string, obj?: any) => { |
| 51 | + console.log(''); |
| 52 | + console.log(msg); |
| 53 | + if (obj) { |
| 54 | + console.log('\n'); |
| 55 | + console.log(obj); |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +function parseJwt (token:string) { |
| 60 | + return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); |
| 61 | +} |
| 62 | + |
| 63 | +async function main() { |
| 64 | + |
| 65 | + log(`Alright, so, for the demo ...`); |
| 66 | + |
| 67 | + const webId = 'http://localhost:3000/ruben/profile/card#me'; |
| 68 | + |
| 69 | + log(`Ruben V., a.k.a. <${webId}>, has some private data in <http://localhost:3000/ruben/private/data>.`); |
| 70 | + |
| 71 | + log(`Of course, he does not want everyone to be able to see all of his private data when they need just one aspect of it. Therefore, Ruben has installed two Views on his data, based on SPARQL filters from a public Catalog. (When and how this is done is out-of-scope for now.)`); |
| 72 | + |
| 73 | + const webIdData = new Store(parser.parse(await (await fetch(webId)).text())); |
| 74 | + const viewIndex = webIdData.getObjects(webId, terms.solid.viewIndex, null)[0].value; |
| 75 | + const views = Object.fromEntries(webIdData.getObjects(viewIndex, terms.solid.entry, null).map(entry => { |
| 76 | + const filter = webIdData.getObjects(entry, terms.solid.filter, null)[0].value; |
| 77 | + const location = webIdData.getObjects(entry, terms.solid.location, null)[0].value; |
| 78 | + return [filter, location]; |
| 79 | + })); |
| 80 | + |
| 81 | + log(`Discovery of views is currently a very crude mechanism based on a public index in the WebID document. (A cleaner mechanism using the UMA server as central hub is being devised.) Using the discovery mechanism, we find the following views on Ruben's private data.`) |
| 82 | + |
| 83 | + log(`(1) <${views[terms.filters.bday]}> filters out his birth date, according to the <${terms.filters.bday}> filter`); |
| 84 | + log(`(2) <${views[terms.filters.age]}> derives his age, according to the <${terms.filters.bday}> filter`); |
| 85 | + |
| 86 | + const policyDir = 'http://localhost:3000/ruben/settings/policies/'; |
| 87 | + |
| 88 | + log(`Access to Ruben's data is based on policies he manages through his Authz Companion app, and which are stored in <${policyDir}>. (This is, of course, not publicly known.)`); |
| 89 | + |
| 90 | + const umaServer = webIdData.getObjects(webId, terms.solid.umaServer, null)[0].value; |
| 91 | + const configUrl = new URL('.well-known/uma2-configuration', umaServer); |
| 92 | + const umaConfig = await (await fetch(configUrl)).json(); |
| 93 | + const tokenEndpoint = umaConfig.token_endpoint; |
| 94 | + |
| 95 | + log(`To request access to Ruben's data, an agent will need to negotiate with Ruben's Authorization Server, which his WebID document identifies as <${umaServer}>.`); |
| 96 | + log(`Via the Well-Known endpoint <${configUrl.href}>, we can discover the Token Endpoint <${tokenEndpoint}>.`); |
| 97 | + |
| 98 | + log(`Now, having discovered both the location of the UMA server and of the desired data, an agent can request the former for access to the latter.`); |
| 99 | + |
| 100 | + log(`...`); |
| 101 | + |
| 102 | + log(`Having been notified in some way of the access request, Ruben could go to his Authz Companion app, and add a policy allowing the requested access.`); |
| 103 | + |
| 104 | + const privateResource = "http://localhost:3000/ruben/private/derived/age" |
| 105 | + const claim_token = "http://localhost:3000/demo/public/bday-app" |
| 106 | + |
| 107 | + const content = { |
| 108 | + grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket', |
| 109 | + claim_token: encodeURIComponent(claim_token), |
| 110 | + claim_token_format: 'urn:solidlab:uma:claims:formats:webid', |
| 111 | + // ticket, |
| 112 | + permissions: [{ |
| 113 | + resource_id: privateResource, |
| 114 | + resource_scopes: [ 'urn:example:css:modes:read' ], |
| 115 | + }] |
| 116 | + }; |
| 117 | + |
| 118 | + console.log(`=== Requesting token at ${tokenEndpoint} with ticket body:\n`); |
| 119 | + console.log(content); |
| 120 | + console.log(''); |
| 121 | + |
| 122 | + const asRequestResponse = await fetch(tokenEndpoint, { |
| 123 | + method: "POST", |
| 124 | + headers: { |
| 125 | + "content-type":"application/json" |
| 126 | + }, |
| 127 | + body: JSON.stringify(content), |
| 128 | + }) |
| 129 | + |
| 130 | + // For debugging: |
| 131 | + // console.log("Authorization Server response:", await asRequestResponse.text()); |
| 132 | + // throw 'stop' |
| 133 | + |
| 134 | + const asResponse = await asRequestResponse.json(); |
| 135 | + |
| 136 | + const decodedToken = parseJwt(asResponse.access_token); |
| 137 | + |
| 138 | + console.log(`= Status: ${asRequestResponse.status}\n`); |
| 139 | + console.log(`= Body (decoded):\n`); |
| 140 | + console.log({ ...asResponse, access_token: asResponse.access_token.slice(0,10).concat('...') }); |
| 141 | + console.log('\n'); |
| 142 | + |
| 143 | + // for (const permission of decodedToken.permissions) { |
| 144 | + // console.log(`Permissioned scopes for resource ${permission.resource_id}:`, permission.resource_scopes) |
| 145 | + // } |
| 146 | + |
| 147 | + console.log(`=== Trying to create private resource <${privateResource}> WITH access token.\n`); |
| 148 | + |
| 149 | + const tokenResponse = await fetch(privateResource, { |
| 150 | + headers: { 'Authorization': `${asResponse.token_type} ${asResponse.access_token}` } |
| 151 | + }); |
| 152 | + |
| 153 | + console.log(`= Status: ${tokenResponse.status}\n`); |
| 154 | + console.log(`= Body:\n`); |
| 155 | + console.log(`= Body: ${await tokenResponse.text()}`); |
| 156 | + console.log(`\n`); |
| 157 | +} |
| 158 | + |
| 159 | +main(); |
0 commit comments