diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6d68aeaf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.gitignore b/.gitignore index b3c11572..257bbd16 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,4 @@ tmp # Misc .DS_Store +.vscode/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..912b5550 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:20.0.0 +ENV NODE_ENV=production +WORKDIR /usr/src/app +# COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"] +# RUN npm install -g yarn + +COPY . . + +ENV YARN_VERSION 4.0.0 +RUN yarn policies set-version $YARN_VERSION + +RUN corepack enable yarn +RUN yarn install +# COPY . . + +RUN yarn build +RUN yarn install:demo +RUN yarn build:demo + +EXPOSE 3000 +EXPOSE 4000 +EXPOSE 4444 +EXPOSE 5123 +EXPOSE 8201 +EXPOSE 8202 +EXPOSE 8203 + +RUN chown -R node /usr/src/app +USER node +CMD ["yarn", "start:demo"] diff --git a/README.md b/README.md index 67ab303b..53c196dd 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,179 @@ # SolidLab's User Managed Access -This repository contains SolidLab research artefacts on use of UMA in the Solid ecosystem. +This repository contains a demonstrator for the [SolidLab project](https://solidlab.be/) on managing trust-flows in decentralized data storage systems such as Solid. -## Packages +## Cloning the repository -- [`@solidlab/uma`](packages/uma): Experimental and opinionated implementation of [UMA Grants](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html) and [UMA Federation](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-federated-authz-2.0.html). - -- [`@solidlab/uma-css`](packages/css): UMA modules for the [Community Solid Server](https://github.com/CommunitySolidServer/CommunitySolidServer/). - -- [`@solidlab/ucp`](packages/ucp): Usage Control Policy decision/enforcement component. +To run the demonstrator, you will have to clone the repository. +``` +git clone -b e2e/setup git@github.com:SolidLabResearch/user-managed-access.git +cd user-managed-access/ +``` ## Getting started -In order to run this project you need to perform the following steps. - -1. Ensure that you are using Node.js 20 or higher, e.g. by running `nvm use`. (see [.nvmrc](./.nvmrc)) -1. Enable Node.js Corepack with `corepack enable`. -1. Run `yarn install` in the project root (this will automatically call `yarn build:all`). -1. Run `yarn start:all`. - -This will boot up a UMA server and compatible Community Solid Server instance. - -You can then execute the following flows: - -- `yarn script:public`: `GET` the public `/alice/profile/card` without redirection to the UMA server; -- `yarn script:private`: `PUT` some text to the private `/alice/private/resource.txt`, protected by a simple WebID check; -- `yarn script:uma-ucp`: `PUT` some text to the private `/alice/other/resource.txt`, protected by a UCP enforcer checking WebIDs according to policies in `packages/uma/config/rules/policy/`. -- `yarn script:registration`: `POST`, `GET` and `DELETE` some text to/from `/alice/public/resource.txt` to test the correct creation and deletion of resource registrations on the UNA server. -- `yarn script:ucp-enforcement`: Run the UCP enforcer in a script (`scripts/test-ucp-enforcement.ts`). This does not need the servers to be started. +### Setting up the project yourself -`yarn script:flow` runs all flows in sequence. +Before starting, make sure you are on the correct branch (e2e/setup). +See the above command to clone only the relevant branch for the demonstrator. +In order to run the demonstrator you need to perform the following steps. -## Demonstration - -A more extensive example of a real life use case has been implemented as described in [./demo/README.md](./demo/README.md). +1. Ensure that you are using Node.js 20 or higher, e.g. by running `nvm use`. (see [.nvmrc](./.nvmrc)) +2. Enable Node.js Corepack with `corepack enable`. +3. Run `yarn install` in the project root to install the requirements for the Solid server . +4. Run `yarn build` in the project root to build the Solid server. +5. Run `yarn install:demo` in the project root to install the requirements for the demonstrator sites and services . +6. Run `yarn build:demo` in the project root to build the demonstrator sites and services . +7. Run `yarn start:demo` to start both the Solid server and all demonstrator sites and services. + +This will boot up a UMA server and compatible Community Solid Server instance, as well as all sites and services for the demonstrator. + + +### Using docker +There is also a `docker` setup available, for which you need to have docker installed: +``` +docker pull raddecke/solidlab-trust-flows-demo +docker run -p 3000:3000 -p 4000:4000 -p 4444:4444 -p 5123:5123 -p 8201:8201 -p 8202:8202 -p 8203:8203 --net=host raddecke/solidlab-trust-flows-demo:latest +``` + +This will start up the same services as the above system installation. + + +## Screencast + +A screencast of the demonstrator can be found here: https://pod.rubendedecker.be/scholar/screencasts/trust-flows-demo.mp4 +or the video file can be found in the [github repository](https://github.com/SolidLabResearch/user-managed-access/tree/e2e/setup/screencast) + + + +# Demonstrator + +## The user data space with CSS and UMA + +- Ruben V., a.k.a. ``, has retrieved a credential containing their birth date from a government service at ``, and this credential has been stored at `` as a private resource. +- Additionally, an accompanying policy was also retrieved and stored in the policy directory at ``. This ODRL policy governs the access and usage requirements for the age credential resource. +- This can be checked on the companion app at ``. +- Using the login email `ruben@example.org` with the password `abc123`, the companion app shows an overview of the available credentials (the birth date credential) en policies in the data space (one policy managing read access for the user, and another one providing access to the age credential for the purpose of age-verification). + +- Access to Ruben's data is based on policies, which he manages through his Authz Companion app, and which are stored in ``. (This is, of course, not publicly known.) To request access to Ruben's data, an agent will need to negotiate with Ruben's UMA Authorization Server, which his WebID document identifies as ``. Via the Well-Known endpoint ``, we can discover the Token Endpoint ``. + +- 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. We get different results depending on the situation: + + - Without a policy allowing the access, the access is denied. + + However, the UMA server enables multiple flows in which such a policy can be added, for example by notifying the resource owner. (This is out-of-scope for this demo.) 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.` + + - If a policy has been set (and perhaps the agent has been notified in some way to retry the access request), the UMA server will request the following claims from the agent, based on that policy: `http://www.w3.org/ns/odrl/2/purpose` and `urn:solidlab:uma:claims:types:webid`. + + - When the agent has gathered the necessary claims (the manner in which is out-of-scope for this demo), it can send them to the UMA server as a JWT: + + ``` + { + "http://www.w3.org/ns/odrl/2/purpose": "urn:solidlab:uma:claims:purpose:age-verification", + "urn:solidlab:uma:claims:types:webid": "http://localhost:5123/id" + } + ``` + +- Only when a policy is in place and the agent provides the UMA server with the relevant claims, an access token is produced, with which the agent can access the desired data at the Resource Server. + +## The web store + +- The store use-case starts out with the user navigating to a webshop 'The Drinks Center' located at `http://localhost:8202`. +- In here, the user decides to buy a mix of alcoholic and non-alcoholic drinks. +- Before checkout, the user has to first verify their age when alcoholic drinks were added to the cart. +- The list of options here is currently limited to only WebID. +- Upon continuing, we can provide a new WebID, or continue with Ruben's WebID that was stored from some previous interaction. Note that this does not authenticate the user, but only links their WebID to allow the store to negotiate with their system. +- Here, the web store calls their backend service with the WebID value to try and negotiate with the WebID system to find a valid age-credential. + +- In the store backend, a negotiation is setup with the UMA authorization server indicated by the WebID at ``. +- The location of the target resource `` is assumed to be known as an agreed well known path. +- Having discovered both the location of the UMA server and the target resource, the store agent requests access to the latter. + +- As an policy is set for the credential, the UMA server will request the following claims from the agent, based on that policy: `http://www.w3.org/ns/odrl/2/purpose` and `urn:solidlab:uma:claims:types:webid`. +- The store now re-sends the request, passing the following claims as a JWT: + ``` + { + "http://www.w3.org/ns/odrl/2/purpose": "urn:solidlab:uma:claims:purpose:age-verification", + "urn:solidlab:uma:claims:types:webid": "http://localhost:5123/id" + } + ``` +- The UMA server responds on this request by providing an signed JWT containing both an access token that the store agent can use to go retrieve the age credential resource, as well as a usage agreement for what can be done with the data in the following format: + ``` + { + "permissions": [ + { + "resource_id": "http://localhost:3000/ruben/credentials/age-credential", + "resource_scopes": [ "read", "use" ] + } + ], + "contract": { + "@context": [ + "http://www.w3.org/ns/odrl.jsonld", + { + "prov": "http://www.w3.org/ns/prov#" + } + ], + "@type": "Agreement", + "target": "http://localhost:3000/ruben/credentials/age-credential", + "uid": "urn:solidlab:uma:contract:55cfc913-24a3-4134-9895-2fa969a07181", + "assigner": "http://localhost:3000/ruben/profile/card#me", + "assignee": "http://localhost:5123/id", + "permission": [ + { + "action": [ "read", "use" ], + "constraint": [ + { + "leftOperand": "dateTime", + "operator": "gt", + "rightOperand": "2024-05-22T13:42:45.397Z" + }, + { + "leftOperand": "dateTime", + "operator": "lt", + "rightOperand": "2024-05-28T22:00:00.000Z" + }, + { + "leftOperand": "purpose", + "operator": "eq", + "rightOperand": "urn:solidlab:uma:claims:purpose:age-verification" + } + ] + } + ] + } + } + ``` + +- Using the provided token, the store can now retrieve the age credential using this token, after which the age is verified to be over 18, and both the data and contract are stored for auditing purposes. (this storage for auditing purposes is currently not yet negotiated) +- Based on the OK from the back-end, the store allows the user to go forward to the payment screen, where the transaction can be completed. + +## The auditing platform +To complete our trust interaction, we now need a way to check how the web store uses our data in their back-end. +Here, the auditing process can make use of the usage agreement that was obtained by the store backend during the negotiation for the age credential resource. + +- The auditor navigates to the auditing platform located at `` +- Here, 'The Drinks Central' is registered as a store audited by the platform. +- The platform retrieves all auditing information from the store from the store backend via ``. +- For each entry, the auditing platform can automatically verify: + - the signature of the token containing the usage agreement coming from the user data space. + - the signature of the age credential coming form a trusted government instance. + - the provided age in the credential being over 18 years old. +- For each entry, both the retrieved resource and the usage agreement are displayed on the interface. ## Implemented features +The demonstrator contains a demonstration that partially or fully includes the following features. + +### The Solid Server +The demonstrator uses the [Community Solid Server](https://github.com/CommunitySolidServer/CommunitySolidServer) to represent a user data storage location and to host the user WebID. -The packages in this project currently only support a fixed UMA AS per CSS RS, and contain only the trivial [AllAuthorizer](packages/uma/src/models/AllAuthorizer.ts) that allows all access. More useful features are coming soon ... +### UMA Redirects for Solid +This codebase makes use of a Solid Server that can redirect to an Authorization server based on an adaptation of the [UMA protocol](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html). ### Usage control policy enforcement @@ -57,9 +187,18 @@ For more information, you can check out its [own repository](https://github.com/ A test script is provided for a CRUD ODRL engine: `yarn script:ucp-enforcement`. In the [script](./scripts/test-ucp-enforcement.ts) a read Usage Control Rule (in ODRL) is present together with N3 interpretation rules. -Then a read request is performed using the engine, which results in a list of grants. This list is then printed to the console. +Then a read request is performed using the engine, which results in a list of grants. +These are then used as the basis of an agreement that is exchanged with the access token, that represents the usage agreement for the data exchange. + +### Verifiable Credential issuing and verification + +The demonstrator provides a mock government service that can issue a credential (currently manually copied in place), and allows the verification of this credential using their WebID. + +### Auditing +The demonstrator presents an auditing platform, that can read and automatically partially verify the grounds of data exchanges happening in the network. ## Next steps -Have a look at the [milestones](https://github.com/SolidLabResearch/user-managed-access/milestones) we set for ourselves, and other [issues](https://github.com/SolidLabResearch/user-managed-access/issues) we would like to solve. +The next step for the demonstrator is going in the direction of [Europe's Digital Identity Wallets](https://ec.europa.eu/digital-building-blocks/sites/display/EUDIGITALIDENTITYWALLET/EU+Digital+Identity+Wallet+Home) +where we will try to demonstrate how decentralized storage such as Solid can form a strong basis for the storage and sharing of digital crendentials. diff --git a/Requirements.md b/Requirements.md new file mode 100644 index 00000000..fdff2758 --- /dev/null +++ b/Requirements.md @@ -0,0 +1,119 @@ +# TODOs for end-to-end requirements: + +## Final sprint + +- [ ] load generic and instantiated policies in auth frontend +- [ ] update continuation screens in the shop frontend +- [ ] MAKE THE VIDEO + - [ ] show credential, policies -> buy item -> show instantiation that has been added for the user -> show auditing trail +- [ ] Write setup requirements +- [ ] Create new SolidLabResearch repository that links to the e2e/setup branch + +### To Fix By Demo + +- [ ] Add Policy Screen update +- [ ] Final fixes generic policy +- [ ] Change trust display on auditing screen + - [ ] Change contract to "Instantiated Policy" + - [ ] Instantiated Policy -> Trusted instead of verified, age keep verified +- [ ] Auth app -> My pod app + - [ ] My Data + - [ ] My Policies + - [ ] Relevant linking? +- [X] Login information on every App: + - [X] Green -> You are logged in\ + - [X] Red -> You are not logged in + - [X] Blue -> Auditer 3 is logged in +- [ ] Store login buttons: + - [ ] Remove its'me option + - [ ] Continue as Ruben -> Share WebID link (with profile avatar) (This is not a Login!) + + + + + +### HAS TO HAPPEN +- [X] VC and token validation on the auditing frontend + - [X] Represent this with green checkmarks in the frontend +- [ ] Check policy models + +### If there is time +- [ ] Check policy evaluation system + - [ ] Do time related policies work? + - [ ] Can we include wrong purposes that fail? + - [ ] Can we do a check on store registration +- [ ] Store decision to give purchase access or not in the audit entry? + +### If there is a lot of time +- [ ] Pod-based logging (not super necessary atm?) +- [ ] Can we model accesses by 2 different people? + +## Assignment minimum requirements +- [X] The system needs to facilitate the exchange of the data (date of birth). + - [X] A date of birth must be available at some location in the dataspace +- [X] The system needs to provide the store with the trust that the data is correct. + - [X] The stored DOB must be a verifiable credential + - [X] The stored credential must be verifiable on the store backend +- [X] The system needs to provide the person with the trust that their data will only be used for age checking. + - [X] The policy system must be able to handle a purpose +- [ ] The system allows the person to specify in advance the generic policy that “all Belgian stores are allowed to read my date of birth”. + - [X] The system needs to be able to store a generic policy + - [X] An interface needs to be available to store this policy + - [ ] The policy must be modeled in an appropriate way +- [X] The system automatically instantiates the above generic policy into the concrete case that “MyBelgianWineStore is allowed to use my date of birth from 2024-03-01 to 2024-03-15 for the purpose of age verification for purchases” + - [ ] MOCKED -> double check though +- [X] The system allows the above interaction to take place without the person having to click on any dialogs. + - [X] The interaction is automatic after a WebID button is clicked to show what is happening. +- [ ] The system allows the store to prove that they were allowed to perform the age verification. + - [X] A backend storage must be in place for the store + - [X] The store website must forward data storage and checks to the backend +- [X] The system allows the person to check that their data was used correctly. + - [X] An auditing routine must be built in the store backend + - [X] An auditing routine must be built as a frontend interface +- [ ] The Government VC Service + - [X] Must be able to create a VC + - [X] VC must be transfered to demo pod storage -> Not required for Demo because of fixed keypair seed + - [ ] VCs can be validated on the backend of the store +- [ ] The Auditing use-case + - [X] The store backend provides the option to retrieve all required data to audit + - [ ] This can be represented in an auditing browser app that shows colors when verified (token + VC) + + +Small note with using the UMA server token signature as the contract signature. +We can only trace this back to the UMA Server, and cannot reliably check the connection between the WebID and the UMA Server + +Another idea: preemptive auditing: +- The store has to advertise who is auditing them +- The contract has to be signed both ways +- upon agreement, the data is sent to the store AND to the auditing service. +- on auditing, the service can check if the store is withholding information + + + +## Demonstrator requirements +- [ ] Protocol message modelling + - [ ] claim request messages + - [ ] claim provision messages +- [ ] Logging system (no hard requirement) + - [X] Create logging interface + - [ ] Log Instantiated Policies + - [ ] Log Access Grants + - [ ] Log Operations +- [ ] Authorization system + - [ ] include logging endpoint + - [ ] include authorization endpoint + - [ ] include policy management endpoint +- [X] Mock Policy instantiation + - [ ] Write out policy model that works for demo + - [X] ??? Discover existing policies to instantly grant some access + - [ ] Link generic - instantiated - grant - operation +- [x] Negotiation implementations + - [X] Return instantiated policy requirements from ticket resolving function to create a signed instantiated policy to return +- [ ] Signatures + - [ ] Create a VC form an instantiated policy - I use the return JWT as a free signature + - [ ] Create verification endpoint for issued VCs +- [ ] Government mockup + - [ ] Create verification endpoint for issued VCs (can be mocked) +- [ ] Client + - [ ] Make some mock-up of how storage could be handled in a way that allows for auditing + - [ ] Recurring requests make use of the same grant? \ No newline at end of file diff --git a/demo/README.md b/demo/README.md deleted file mode 100644 index 48f916d4..00000000 --- a/demo/README.md +++ /dev/null @@ -1,36 +0,0 @@ - -# Demonstration - -Using the UMA server implemented in this repository, we set up an extensive demonstration of a real life use case: age verification for online shops selling age-restricted goods, such as alcoholic beverages. - -To experiment with the demo, first build the necessary extra code with `build:demo`, then start the demo by running `start:demo`. This starts the CSS and UMA servers with the right configurations, and spins up two websites: an online shop on `http://localhost:5001`, and a policy manager on `http://localhost:5002`. - -The context "story" of the demonstration is the following. This "story" can be either run through via the graphical interfaces of the websites, or by running the script `yarn script:demo`. - -- Ruben V., a.k.a. ``, has some private data in ``. 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.) - -- 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 underway.) Using this discovery mechanism, we can find the following views on Ruben's private data: - - 1. `` filters out his birth date, according to the `` filter; - 2. `` derives his age, according to the `` filter. - -- Access to Ruben's data is based on policies, which he manages through his Authz Companion app, and which are stored in ``. (This is, of course, not publicly known.) To request access to Ruben's data, an agent will need to negotiate with Ruben's UMA Authorization Server, which his WebID document identifies as ``. Via the Well-Known endpoint ``, we can discover the Token Endpoint ``. - -- 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. We get different results depending on the situation: - - - Without a policy allowing the access, the access is denied. - - However, the UMA server enables multiple flows in which such a policy can be added, for example by notifying the resource owner. (This is out-of-scope for this demo.) 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.` - - - If a policy has been set (and perhaps the agent has been notified in some way to retry the access request), the UMA server will request the following claims from the agent, based on that policy: `http://www.w3.org/ns/odrl/2/purpose` and `urn:solidlab:uma:claims:types:webid`. - - - When the agent has gathered the necessary claims (the manner in which is out-of-scope for this demo), it can send them to the UMA server as a JWT: - - ``` - { - "http://www.w3.org/ns/odrl/2/purpose": "urn:solidlab:uma:claims:purpose:age-verification", - "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/demo/public/vendor" - } - ``` - -- Only when a policy is in place and the agent provides the UMA server with the relevant claims, an access token is produced, with which the agent can access the desired data at the Resource Server. diff --git a/demo/backend/gov-vc-issuer/package.json b/demo/backend/gov-vc-issuer/package.json new file mode 100644 index 00000000..28c41fdd --- /dev/null +++ b/demo/backend/gov-vc-issuer/package.json @@ -0,0 +1,47 @@ +{ + "name": "solid-vc-service", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "keywords": [], + "author": "Gertjan De Mulder (gertjan.demulder@ugent.be), Ruben Dedecker", + "license": "MIT", + "devDependencies": { + "@types/express": "^4.17.21", + "@types/jsonld": "^1.5.13", + "@types/node": "^20.11.30", + "@types/streamify-string": "^1.0.4", + "nodemon": "^3.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.3" + }, + "dependencies": { + "@digitalbazaar/data-integrity": "^2.1.0", + "@digitalbazaar/data-integrity-context": "^2.0.0", + "@digitalbazaar/ed25519-multikey": "^1.1.0", + "@digitalbazaar/ed25519-signature-2020": "^5.2.0", + "@digitalbazaar/eddsa-2022-cryptosuite": "^1.0.0", + "@digitalcredentials/vc-data-model": "^1.1.1", + "@inrupt/solid-client": "^2.0.1", + "@inrupt/solid-client-authn-core": "^2.1.0", + "@solid/community-server": "^7.0.4", + "cors": "^2.8.5", + "credentials-context": "^2.0.0", + "express": "^4.19.1", + "jsonld": "^8.3.2", + "jsonld-document-loader": "^2.0.0", + "jsonld-signatures": "^11.2.1", + "mime-types": "^2.1.35", + "n3": "^1.17.3", + "rdf-parse": "^2.3.3", + "rdf-serialize": "^2.2.3", + "stream-to-string": "^1.2.1", + "streamify-string": "^1.0.1", + "url-join": "^5.0.0" + } +} diff --git a/demo/backend/gov-vc-issuer/src/config.ts b/demo/backend/gov-vc-issuer/src/config.ts new file mode 100644 index 00000000..4e69ff31 --- /dev/null +++ b/demo/backend/gov-vc-issuer/src/config.ts @@ -0,0 +1,6 @@ +export const config = { + port: 4444, + name: 'vc-service', + // https://expressjs.com/en/resources/middleware/cors.html + cors: {} // Allow all origin +} diff --git a/demo/backend/gov-vc-issuer/src/controller/index.ts b/demo/backend/gov-vc-issuer/src/controller/index.ts new file mode 100644 index 00000000..fd28753d --- /dev/null +++ b/demo/backend/gov-vc-issuer/src/controller/index.ts @@ -0,0 +1,342 @@ +import {CredentialSubject, VCDIVerifiableCredential} from "@digitalcredentials/vc-data-model/dist/VerifiableCredential"; +// @ts-ignore +import cred from 'credentials-context'; +// @ts-ignore +import jsigs from 'jsonld-signatures'; +// @ts-ignore +import {JsonLdDocumentLoader} from 'jsonld-document-loader'; +//@ts-ignore +import {cryptosuite as eddsa2022CryptoSuite} from '@digitalbazaar/eddsa-2022-cryptosuite'; +//@ts-ignore +import {DataIntegrityProof} from '@digitalbazaar/data-integrity'; +import {getCurrentDateTime} from "../utils/index.js"; +import {ExportKeyParameters, IDidDocument, K, SerializedKeyPair, SignParameters, VerifyParameters} from "../interfaces"; +//@ts-ignore +import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey'; +// @ts-ignore +import dataIntegrityContext from '@digitalbazaar/data-integrity-context'; +import jsonld from "jsonld"; +import {AccessModes, getResourceInfo, overwriteFile, universalAccess, UrlString} from "@inrupt/solid-client"; +import { randomUUID } from "node:crypto"; + +const {contexts: credentialsContexts, constants: {CREDENTIALS_CONTEXT_V1_URL}} = + cred; +const {purposes: {AssertionProofPurpose}} = jsigs; + +export function isVC(o: any) { + + let valid = false; + let validationError = undefined + try { + // check type presence and cardinality + if(!o.type) { + throw new Error('"type" property is required.'); + } + if(!o.credentialSubject) { + throw new Error('"credentialSubject" property is required.'); + } + if(!o.issuer) { + throw new Error('"issuer" property is required.'); + } + // No errors thrown? Valid! + valid = true + + } catch (error: any) { + validationError = error.toString() + } finally { + return {valid, validationError} + } + + +} + +/** + * [Depends on key implementations] + * @param key + */ +function createSignSuite(key: K) { + return new DataIntegrityProof({ + signer: key.signer!(), + cryptosuite: eddsa2022CryptoSuite + }) +} + +/** + * [Depends on key implementations] + */ +function createVerifySuite() { + return new DataIntegrityProof({ + cryptosuite: eddsa2022CryptoSuite + }); +} +/** + * CONTROLLER FUNCTIONS + * @param params + */ +export async function sign(params: SignParameters) { + + const {documentLoader, key, credential} = params + // const suite = new Ed25519Signature2020({key}) + + const suite = createSignSuite(key) + const signedCredential = await jsigs.sign(credential, { + suite, + purpose: new AssertionProofPurpose(), + documentLoader + }); + return signedCredential +} + +/** + * + * @param params + */ +export async function verify(params: VerifyParameters) { + const {credential, documentLoader} = params + const validationResult = isVC(credential) + + const suite = createVerifySuite() + + const verifyParams = { + suite, + purpose: new AssertionProofPurpose(), + documentLoader + } + + const verificationResult = await jsigs.verify( + credential, + verifyParams + ) + + return { + validationResult, + verificationResult + } +} + +export function createCredential( + k: K, + credentialSubject: CredentialSubject, + description?: string + ): VCDIVerifiableCredential { + const c: VCDIVerifiableCredential = { + '@context': [CREDENTIALS_CONTEXT_V1_URL, { "dc": "http://purl.org/dc/terms/" }], + id: `urn:gov.flanders.be:credentials:${randomUUID()}`, + type: ['VerifiableCredential'], + issuer: k.controller!, + issuanceDate: getCurrentDateTime(), + credentialSubject: credentialSubject, + } + if(description) c['dc:description'] = description; + return c +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// HELPERS +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +export function getKeypairUrl(webId: string): string { + return webId.replace('/profile/card#me', '/keypair') +} +export function getKeyControllerDocumentUrl(webId: string): string { + return webId.replace('/card#me','/key') +} +export function getCardUrl(webId: string) :string { + return webId.replace('#me','') +} +export async function storeKeypairOnSolidPod(key: K, webId: string, authFetch: any) { + // Upload key to pod (private location) + const privateExport = await exportKeypair(key, + { publicKey: true, secretKey: true, includeContext: true}) + + const urlKeypair = getKeypairUrl(webId) + let response = await authFetch(urlKeypair, { + method: 'PUT', + headers: {'content-type': 'application/json'}, + body: JSON.stringify(privateExport) + }); + + if(!response.ok) + throw new Error('Failed to upload keypair!') +} +export async function fetchKeypairFromSolidPod(webId: string, authFetch: any) { + // Get keypair -- url convention determined by getKeypairUrl + const urlKeypair = getKeypairUrl(webId) + const response = await authFetch(urlKeypair) + + let kpResource = await response.json() + + const kp = await Ed25519Multikey.from(kpResource) + // const kp = await Ed25519VerificationKey2020.fromKeyDocument({document: kpResource as any}) + return kp + +} + +/** + * [Depends on key implementations] + * @param webId + * @param password + */ +export async function createKey(webId: string, password: string, keyUrl: string): Promise { + + let seed = new Uint8Array(32) + seed = Buffer.alloc(32, Uint8Array.from(Buffer.from(password))) + const k = await Ed25519Multikey.generate({seed, + id: keyUrl, + controller: webId + }) + + return k +} + +export function preloadDocumentLoaderContexts(jdl: JsonLdDocumentLoader) { + jdl.addStatic( + CREDENTIALS_CONTEXT_V1_URL, + credentialsContexts.get(CREDENTIALS_CONTEXT_V1_URL) + ) + + jdl.addStatic( + dataIntegrityContext.constants.CONTEXT_URL, + dataIntegrityContext.contexts.get(dataIntegrityContext.constants.CONTEXT_URL) + ); +} + + +/** + * + * @param k + * @param embedVerificationMethod: if true, keymaterial will be embedded Otherwise, url + */ +export function createControllerDocument(k : any, embedVerificationMethod = false): IDidDocument { + const vm = embedVerificationMethod ? k : k.id + const controllerDocument = { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": k.controller, + "verificationMethod": [ + vm + ], + "assertionMethod": [ + k.id + ] + } + return controllerDocument +} + + +export const addControllerDocumentToCard = async function( + authFetch: typeof fetch, + webId: string, + controllerDoc: any +){ + const kRdf = await jsonld.toRDF( + controllerDoc, {format: 'application/n-quads'} + ) + + const patchInsert = Object(kRdf).toString() + const url= getCardUrl(webId) + await n3patch( + url, + authFetch, + undefined, + patchInsert + ) +} + +/** + * Add file to a Solid Pod Container. + * @param urlContainer + * @param data + * @param mimeType + * @param slug + * @param publicAccess + */ +export async function addFileToContainer( + authFetch: typeof fetch, + urlContainer: string, + data: Buffer, + mimeType = 'application/ld+json', + slug: string, + publicAccess?: AccessModes +) { + + const file = new Blob([data]) + const fileUrl = new URL(slug, urlContainer).toString() as UrlString + + await overwriteFile( + fileUrl, + file, + {contentType: mimeType, fetch: authFetch} + ) + + const serverResourceInformation = await getResourceInfo(fileUrl, {fetch: authFetch}) + if (publicAccess!!) { + await universalAccess.setPublicAccess(serverResourceInformation.internal_resourceInfo.sourceIri, publicAccess!, {fetch: authFetch}) + } + return serverResourceInformation.internal_resourceInfo.sourceIri + +} + +export async function exportKeypair(key: K, params: ExportKeyParameters): Promise { + /// ⚠️ Note: export params depend on implementation of the key. + // For example, to indicate the export of the private/secret key, + // @digitalbazaar/ed25519-multikey uses secretKey, while + // @digitalcredentials/ed25519-verification-key-2020 uses privateKey + return await key.export(params) +} +/** + * Spec: https://solid.github.io/specification/protocol#writing-resources + * @param url + * @param where + * @param inserts + * @param deletes + * @param prefixes + */ +export async function n3patch(url: string, + authFetch: Function, + where?: string, + inserts?: string, + deletes?: string, + prefixes?: Record + +) { + + const clauses = [ + where ? `solid:where { ${where} }` : where, + inserts ? `solid:inserts { ${inserts} }` : inserts, + deletes ? `solid:deletes { ${deletes} }` : deletes, + ].filter(c => c!!).join(';\n') + + + const n3Patch = ` + @prefix solid: . + ${ + prefixes! ? Object.entries(prefixes!).map(([p, ns]) => `@prefix ${p}: <${ns}> .`).join('\n') : '' + } + + _:rename a solid:InsertDeletePatch; + ${clauses} + . + ` + + const response = await authFetch( + url, + { + method: 'PATCH', + headers: { + 'content-type': "text/n3" + }, + body: n3Patch + } + ) + + const {ok, status, statusText} = response + if (!ok) + throw new Error(` + N3 Patch failed. + Url: ${url} + Status: ${status} - ${statusText} + N3 Patch:\n${n3Patch} + `) + +} diff --git a/demo/backend/gov-vc-issuer/src/index.ts b/demo/backend/gov-vc-issuer/src/index.ts new file mode 100644 index 00000000..ca7663b7 --- /dev/null +++ b/demo/backend/gov-vc-issuer/src/index.ts @@ -0,0 +1,159 @@ +import express, {Express} from 'express'; +import bodyParser from "body-parser"; +import cors from 'cors' + +// @ts-ignore +import {JsonLdDocumentLoader} from 'jsonld-document-loader'; +import {CredentialSubject} from "@digitalcredentials/vc-data-model/dist/VerifiableCredential"; +import {createDocumentLoader, getAuthenticatedFetch, parseToJsonLD} from './utils/index.js'; +import {config} from './config.js' +import { + createCredential, + createKey, + exportKeypair, + preloadDocumentLoaderContexts, + sign, + verify +} from "./controller/index.js"; + +// WebID shenanigans +import rdfParser from 'rdf-parse' +import rdfSerializer from 'rdf-serialize' +import Streamify from 'streamify-string' +import streamToString from 'stream-to-string' + +const serviceUrl = 'http://localhost:4444/' +const keyUrl = serviceUrl + 'key' +const webIdDocument = serviceUrl + 'id' +const webId = webIdDocument + '#me' +const pass = 'abc123' + +const jdl = new JsonLdDocumentLoader(); +preloadDocumentLoaderContexts(jdl) + + +const key = await createKey(webId, pass, keyUrl) + +// Add key description to Solid WebID Profile Document +const publicKeyExport = await exportKeypair(key, {publicKey:true, includeContext:true}) + + +const app: Express = express(); +app.use(cors(config.cors)) +app.use(bodyParser.json()) +app.use((req, res, next) => { + const {method, url} = req + console.log(`[${config.name}] ${method}\t${url}`) + next() +}) + +app.get('/id', async (req,res)=>{ + + const ttlDocument = +`@prefix foaf: . +@prefix solid: . + +<${webIdDocument}> a foaf:PersonalProfileDocument; + foaf:maker <${webId}>; + foaf:primaryTopic <${webId}>. +<${webId}> a foaf:Organization; + foaf:name "Flemish Government VC Registry"; + <${keyUrl}>; + <${keyUrl}>.` + + // respond with html page + if (req.accepts('text/turtle')) { + res.status(200) + res.contentType('text/turtle') + res.send(ttlDocument) + return; + } + + let outputContentType; + + // respond with json + if (req.accepts('application/json')) { + outputContentType = 'application/ld+json' + } else if (req.accepts('text/html')) { + outputContentType = 'application/ld+json' + } else { + if (req.accepted) outputContentType = req.accepted[0].value + else if (req.headers['accept']) outputContentType = req.headers['accept'] + else outputContentType = 'text/turtle' + } + + const textStream = Streamify(ttlDocument) + + // .default because of some typing errors + const quadStream = await (rdfParser as any).default.parse(textStream, { contentType: 'text/turtle' }) + const newTextStream = await (rdfSerializer as any).default + .serialize(quadStream, { contentType: outputContentType }); + + const resultingDocument = await streamToString(newTextStream) + + res.status(200) + res.contentType(outputContentType) + res.send(resultingDocument) +}) + +app.get('/key', async (req,res)=>{ + res.header('Content-Type', 'application/json') + res.send(Buffer.from(JSON.stringify(publicKeyExport,null,2))) +}) + + +app.get('/credential', async (req,res)=>{ + const { webid } = req.query; + + if (!webId) { + res.status(400) + res.send('This request requires a webid parameter') + } + + const credentialSubject = { + "@id": webid, + "http://www.w3.org/2006/vcard/ns#bday": { + "@value": new Date('1995-04-09').toISOString(), + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + } + } as CredentialSubject + + const credential = createCredential( + key, + credentialSubject, + "Age credential issued by Flemish Government" + ) + + console.log('credential', credential) + + const signedCredential = await sign({ + key, + credential, + documentLoader: createDocumentLoader(jdl) + }) + + console.log('signedCredential', signedCredential) + + res.setHeader('content-type', 'application/json') + res.send(JSON.stringify(signedCredential)) + +}) + + +app.post('/verify', async (req,res)=>{ + const verifiableCredential = req.body + const documentLoader = createDocumentLoader(jdl) + + const {validationResult, verificationResult} = await verify({ + credential: verifiableCredential, + documentLoader + }) + + console.log('result', JSON.stringify(validationResult, null, 2), JSON.stringify(verificationResult, null, 2)) + + res.send({validationResult, verificationResult}) +}) + +app.listen(config.port, () => { + console.log(`⚡️[${config.name}]: Server is running at http://localhost:${config.port}`); +}) diff --git a/demo/backend/gov-vc-issuer/src/interfaces/index.ts b/demo/backend/gov-vc-issuer/src/interfaces/index.ts new file mode 100644 index 00000000..ea6ec3dc --- /dev/null +++ b/demo/backend/gov-vc-issuer/src/interfaces/index.ts @@ -0,0 +1,108 @@ +import {VCDIVerifiableCredential} from "@digitalcredentials/vc-data-model/dist/VerifiableCredential"; + +export interface IKeyExport { + type: string + id: string + controller: string + publicKeyMultibase: string + privateKeyMultibase: string +} + +export interface IVerificationMethod { + id: string + controller: string + type: string + publicKeyJwk?: object + publicKeyMultibase?: string + publicKeyBase58?: string +} + +export interface IServiceEndpoint { + id: string + type: string | string[] + serviceEndpoint: string | string[] +} + +/** + * https://www.w3.org/TR/did-core/#did-document-properties + */ +export interface IDidDocument { + '@context': string | string[] + id: string + alsoKnownAs?: string | string[] + controller?: string | string[] + + // Verification Methods + verificationMethod?: (IVerificationMethod | string)[] + authentication?: (IVerificationMethod | string)[] + assertionMethod?: (IVerificationMethod | string)[] + keyAgreement?: (IVerificationMethod | string)[] + capabilityInvocation?: (IVerificationMethod | string)[] + capabilityDelegation?: (IVerificationMethod | string)[] + + service?: (IServiceEndpoint | string)[] +} + +export type DocumentLoaderResponse = { + contextUrl: null | string + documentUrl: null | string + document: any +} + +export interface IDocumentLoader { + (url: any): Promise +} + +/** + * TYPES + */ +export type BaseParameters = { + documentLoader: any +} +export type K = KeyPair +export type SignParameters = BaseParameters & { + credential: VCDIVerifiableCredential + key: K +} +export type VerifyParameters = BaseParameters & { + credential: VCDIVerifiableCredential +} +export type ExportKeyParameters = { + publicKey: boolean + includeContext: boolean + secretKey?: boolean // used by @digitalbazaar + privateKey?: boolean // used by @digitalcredentials +} +export type VerifyResult = { + verificationResult: any, + error: any +} + +/** + * INTERFACES + */ +export interface SerializedKeyPair { + '@context'?: string; + id?: string; + type?: string; + controller?: string; + revoked?: string; + /** + * Public / private key material + */ + publicKeyBase58?: string; + privateKeyBase58?: string; + publicKeyMultibase?: string; + privateKeyMultibase?: string; + publicKeyJwk?: object; + privateKeyJwk?: object; +} + +export interface KeyPair extends SerializedKeyPair { + id?: string; + type?: string; + controller?: string; + revoked?: string; + 'export': Function; + signer?: Function +} diff --git a/demo/backend/gov-vc-issuer/src/utils/index.ts b/demo/backend/gov-vc-issuer/src/utils/index.ts new file mode 100644 index 00000000..efeca183 --- /dev/null +++ b/demo/backend/gov-vc-issuer/src/utils/index.ts @@ -0,0 +1,201 @@ +import jsonld from "jsonld"; +import N3 from "n3"; +import urljoin from "url-join"; +import {buildAuthenticatedFetch, createDpopHeader, generateDpopKeyPair} from "@inrupt/solid-client-authn-core"; +import {IDocumentLoader} from "../interfaces"; +// @ts-ignore +import {JsonLdDocumentLoader} from 'jsonld-document-loader'; +import * as util from 'node:util' + +export function printObject(x: any) { + console.log(util.inspect(x,{showHidden: false, depth: null, colors: true})) +} +export async function convertNQuadsToJSONLD(nquads: string): Promise { + + return new Promise(async (resolve, reject) => { + try { + // Note: fromRDF requires an object as input + const jsonldData = await jsonld.fromRDF( + (nquads as unknown) as object, + {format: 'application/n-quads'}); + resolve(jsonldData) + } catch (error) { + reject(error) + } + }) + +} + +export async function ttl2jld(ttl: string, baseIri?: string): Promise { + const store = await ttl2store(ttl, baseIri) + const jld = await jsonld.fromRDF(store,) + return jld +} + +export async function ttl2store(ttl: string, baseIRI?: string): Promise { + const quads = new N3.Parser({ + format: 'text/turtle', + baseIRI + }).parse(ttl); + return new N3.Store(quads) +} + +/// +async function getAuthorisation(email: string, password: string, serverUrl: string) { +// First we request the account API controls to find out where we can log in + const indexResponse = await fetch(urljoin(serverUrl, '.account/')); + const {controls} = await indexResponse.json() as any + +// And then we log in to the account API + const response = await fetch(controls.password.login, { + method: 'POST', + headers: {'content-type': 'application/json'}, + body: JSON.stringify({email: email, password: password}), + }); +// This authorization value will be used to authenticate in the next step + const {authorization} = await response.json() as any + return authorization; +} + +async function generateToken(webId: string, serverUrl: string, authorization: string) { + // First we need to request the updated controls from the server now that we are logged in. + // These will now have more values than in the previous example. + const indexResponse = await fetch(urljoin(serverUrl + '.account/'), { + headers: {authorization: `CSS-Account-Token ${authorization}`} + }); + const {controls} = await indexResponse.json() as any; + + // Here we request the server to generate a token on our account + const response = await fetch(controls.account.clientCredentials, { + method: 'POST', + headers: {authorization: `CSS-Account-Token ${authorization}`, 'content-type': 'application/json'}, + // The name field will be used when generating the ID of your token. + // The WebID field determines which WebID you will identify as when using the token. + // Only WebIDs linked to your account can be used. + body: JSON.stringify({name: 'my-token', webId: webId}), + }); + + // These are the identifier and secret of your token. + // Store the secret somewhere safe as there is no way to request it again from the server! + // The `resource` value can be used to delete the token at a later point in time. + //const { id, secret, resource } = await response.json(); + const token = await response.json() // contains id, secret, resource + return token; +} + +async function requestAccessToken(token: any, serverUrl: string) { + const {id, secret} = token; + + // A key pair is needed for encryption. + // This function from `solid-client-authn` generates such a pair for you. + const dpopKey = await generateDpopKeyPair(); + +// These are the ID and secret generated in the previous step. +// Both the ID and the secret need to be form-encoded. + const authString = `${encodeURIComponent(id)}:${encodeURIComponent(secret)}`; +// This URL can be found by looking at the "token_endpoint" field at +// http://localhost:3000/.well-known/openid-configuration +// if your server is hosted at http://localhost:3000/. + const tokenUrl = urljoin(serverUrl, '.oidc/token'); + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { + // The header needs to be in base64 encoding. + authorization: `Basic ${Buffer.from(authString).toString('base64')}`, + 'content-type': 'application/x-www-form-urlencoded', + dpop: await createDpopHeader(tokenUrl, 'POST', dpopKey), + }, + body: 'grant_type=client_credentials&scope=webid', + }); + +// This is the Access token that will be used to do an authenticated request to the server. +// The JSON also contains an "expires_in" field in seconds, +// which you can use to know when you need request a new Access token. + const {access_token, expires_in} = await response.json() as any; + + const today = new Date(); + today.setSeconds(today.getSeconds() + expires_in); + + return { + accessToken: access_token, + expiresOn: today, + dpopKey + } +} + +export async function getAuthenticatedFetch(email: string, password: string, serverUrl: string, webId: string) { + + console.log('Generating access token'); + const authorisation = await getAuthorisation(email, password, serverUrl); + const token = await generateToken(webId, serverUrl, authorisation); + const {accessToken, dpopKey} = await requestAccessToken(token, serverUrl); + +// The DPoP key needs to be the same key as the one used in the previous step. +// The Access token is the one generated in the previous step. + const authFetch = await buildAuthenticatedFetch(accessToken, {dpopKey}); +// authFetch can now be used as a standard fetch function that will authenticate as your WebID. + console.log("authFetch ready"); + return authFetch; +} + +export async function parseToJsonLD(data: any, contentType: string) { + let parsedData = undefined; + switch (contentType) { + case 'application/n-quads': + parsedData = await convertNQuadsToJSONLD(data) + break; + case 'text/turtle': + parsedData = await ttl2jld(data) + break; + case 'application/json': + case 'application/ld+json': + parsedData = JSON.parse(data) + break + default: + throw new Error(`Content-type: ${contentType} not yet supported!`) + } + return parsedData +} + +export function getCurrentDateTime() { + const now = new Date(); + + const year = now.getUTCFullYear(); + const month = String(now.getUTCMonth() + 1).padStart(2, '0'); // Month is 0-indexed + const day = String(now.getUTCDate()).padStart(2, '0'); + const hours = String(now.getUTCHours()).padStart(2, '0'); + const minutes = String(now.getUTCMinutes()).padStart(2, '0'); + const seconds = String(now.getUTCSeconds()).padStart(2, '0'); + + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`; +} + +export function createDocumentLoader(jdl: JsonLdDocumentLoader): IDocumentLoader { + const dl = jdl.build() + return async (url: string) => { + console.log('loading document loader', url) + let resolvedDocument = undefined + try { + console.log(`🔗\t${url}`) + resolvedDocument = await dl(url) + } catch (error) { + // resolve from network + let document = await (await fetch(url, {headers: {'accept': 'application/json'}})).json() + resolvedDocument = { + contextUrl: null, + document, + documentUrl: url + } + } + if (!resolvedDocument) + throw new Error(`COULD NOT RESOLVE DOCUMENT FOR ${url}`) + return resolvedDocument + } +} + +export function Vocab(ns: string) { + return (p: string) => { + return ns.endsWith('#') || ns.endsWith('/') ? + ns.concat(p) : ns.concat('#', p) + } +} diff --git a/demo/backend/gov-vc-issuer/src/verify.ts b/demo/backend/gov-vc-issuer/src/verify.ts new file mode 100644 index 00000000..08bf8945 --- /dev/null +++ b/demo/backend/gov-vc-issuer/src/verify.ts @@ -0,0 +1,21 @@ +import { createDocumentLoader } from './utils/index.js'; +// @ts-ignore +import { JsonLdDocumentLoader } from 'jsonld-document-loader'; +import { + preloadDocumentLoaderContexts, + verify +} from "./controller/index.js"; + +const jdl = new JsonLdDocumentLoader(); +preloadDocumentLoaderContexts(jdl) + +async function myVerify(verifiableCredential: any) { + const documentLoader = createDocumentLoader(jdl) + + return await verify({ + credential: verifiableCredential, + documentLoader + }) +} + +export default myVerify; diff --git a/demo/backend/gov-vc-issuer/test/css/css.json b/demo/backend/gov-vc-issuer/test/css/css.json new file mode 100644 index 00000000..ddbf61f5 --- /dev/null +++ b/demo/backend/gov-vc-issuer/test/css/css.json @@ -0,0 +1,37 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld", + "import": [ + "css:config/app/init/static-root.json", + "css:config/app/main/default.json", + "css:config/app/variables/default.json", + "css:config/http/handler/default.json", + "css:config/http/middleware/default.json", + "css:config/http/notifications/all.json", + "css:config/http/server-factory/http.json", + "css:config/http/static/default.json", + "css:config/identity/access/public.json", + "css:config/identity/email/default.json", + "css:config/identity/handler/default.json", + "css:config/identity/oidc/default.json", + "css:config/identity/ownership/token.json", + "css:config/identity/pod/static.json", + "css:config/ldp/authentication/dpop-bearer.json", + "css:config/ldp/authorization/webacl.json", + "css:config/ldp/handler/default.json", + "css:config/ldp/metadata-parser/default.json", + "css:config/ldp/metadata-writer/default.json", + "css:config/ldp/modes/default.json", + "css:config/storage/backend/memory.json", + "css:config/storage/key-value/resource-store.json", + "css:config/storage/location/pod.json", + "css:config/storage/middleware/default.json", + "css:config/util/auxiliary/acl.json", + "css:config/util/identifiers/suffix.json", + "css:config/util/index/default.json", + "css:config/util/logging/winston.json", + "css:config/util/representation-conversion/default.json", + "css:config/util/resource-locker/file.json", + "css:config/util/variables/default.json" + ], + "@graph": [] +} diff --git a/demo/backend/gov-vc-issuer/test/css/seeded-pod-config.json b/demo/backend/gov-vc-issuer/test/css/seeded-pod-config.json new file mode 100644 index 00000000..e7b61d84 --- /dev/null +++ b/demo/backend/gov-vc-issuer/test/css/seeded-pod-config.json @@ -0,0 +1,23 @@ +[ + { + "email": "government@belgium.be", + "password": "abc123", + "pods": [ + { "name": "gov-data-registry" } + ] + }, + { + "email": "hello@example.com", + "password": "abc123", + "pods": [ + { "name": "example" } + ] + }, + { + "email": "hello@verifiable-example.com", + "password": "abc123", + "pods": [ + { "name": "verifiable-example" } + ] + } +] diff --git a/demo/backend/gov-vc-issuer/test/issue-verify.test.js b/demo/backend/gov-vc-issuer/test/issue-verify.test.js new file mode 100644 index 00000000..7b927474 --- /dev/null +++ b/demo/backend/gov-vc-issuer/test/issue-verify.test.js @@ -0,0 +1,75 @@ +import fs from 'fs' +import path from 'path' +import assert from "node:assert"; +import * as mime from "mime-types"; + +let apiConfig = { + origin: 'http://localhost', + port:4444, + baseUrl: undefined, + routes: {} +} +apiConfig['baseUrl'] = `${apiConfig.origin}:${apiConfig.port}` +apiConfig.routes = Object.fromEntries(['setup','issue','verify'] + .map(r => [r, new URL(r, apiConfig.baseUrl).toString()])) + +const dirResources = './test/resources' +const inputFiles = fs.readdirSync('./test/resources') + .map(x => { + const fpath = path.resolve(dirResources, x) + const contentType = mime.lookup(fpath) + return { fpath, contentType } + }) + +const podConfig = { + "email": "government@belgium.be", + "password": "abc123", + "css": "http://localhost:8080/", + "webId": "http://localhost:8080/gov-data-registry/profile/card#me" +} +async function main() { + console.log('>>> SETUP') + const setup = async () => { + await fetch(apiConfig.routes.setup,{ + method: 'POST', + headers: {'content-type': 'application/json'}, + body: JSON.stringify(podConfig) + }) + } + await setup() + for await (const f of inputFiles) { + console.log(`Issue & Verify resource: ${f.fpath} (${f.contentType})`) + console.log('>>> ISSUE') + const response = await fetch(apiConfig.routes.issue, { + method:'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + ...podConfig, + data: fs.readFileSync(f.fpath, {encoding:'utf-8'}), + contentType: f.contentType + }) + }) + if(!response.ok) { + throw new Error(`Test failed for ${f.fpath}`) + } + const vc = await response.json() + console.log(`VC\n${JSON.stringify(vc, null, 2)}`) + + // VERIFY + console.log('>>> VERIFY') + const verificationResultResponse = await fetch(apiConfig.routes.verify, { + method:'POST', + headers: { 'content-type': 'application/json'}, + body: JSON.stringify({verifiableCredential: vc}) + }) + const {verificationResult, validationResult} = await verificationResultResponse.json() + assert(validationResult.valid) + assert(verificationResult.verified) + console.log(`${f.fpath} ✅`) + } + +} +main().then().catch(console.error); +export {}; diff --git a/demo/backend/gov-vc-issuer/test/issue.http b/demo/backend/gov-vc-issuer/test/issue.http new file mode 100644 index 00000000..b9837bc9 --- /dev/null +++ b/demo/backend/gov-vc-issuer/test/issue.http @@ -0,0 +1,12 @@ +### TODO: doc +POST http://localhost:4444/issue +Content-Type: application/json + +{ + "email": "government@belgium.be", + "password": "abc123", + "css": "http://localhost:3000/", + "webId": "http://localhost:3000/gov-data-registry/profile/card#me", + "contentType": "application/n-quads", + "data": " ." +} diff --git a/demo/backend/gov-vc-issuer/test/resources/alice-university.jsonld b/demo/backend/gov-vc-issuer/test/resources/alice-university.jsonld new file mode 100644 index 00000000..c443a235 --- /dev/null +++ b/demo/backend/gov-vc-issuer/test/resources/alice-university.jsonld @@ -0,0 +1,4 @@ +{ + "@id": "https://example.edu/students/alice", + "ex:test": "Example University" +} diff --git a/demo/backend/gov-vc-issuer/test/setup.http b/demo/backend/gov-vc-issuer/test/setup.http new file mode 100644 index 00000000..eb44c4f2 --- /dev/null +++ b/demo/backend/gov-vc-issuer/test/setup.http @@ -0,0 +1,10 @@ +### TODO: doc +POST http://localhost:4444/setup +Content-Type: application/json + +{ + "email": "government@belgium.be", + "password": "abc123", + "css": "http://localhost:3000/", + "webId": "http://localhost:3000/gov-data-registry/profile/card#me" +} diff --git a/demo/backend/gov-vc-issuer/tsconfig.json b/demo/backend/gov-vc-issuer/tsconfig.json new file mode 100644 index 00000000..ca3ecd3e --- /dev/null +++ b/demo/backend/gov-vc-issuer/tsconfig.json @@ -0,0 +1,107 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "ES2022", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "outDir": "dist" + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "./coverage", + "./dist", + "__tests__", + "jest.config.ts", + "./src/verify.ts" + ] +} diff --git a/demo/backend/store/package-lock.json b/demo/backend/store/package-lock.json new file mode 100644 index 00000000..7bcd9d0b --- /dev/null +++ b/demo/backend/store/package-lock.json @@ -0,0 +1,861 @@ +{ + "name": "store-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "store-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.19.2", + "n3": "^1.17.3" + }, + "devDependencies": { + "typescript": "^5.4.5" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/n3": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/n3/-/n3-1.17.3.tgz", + "integrity": "sha512-ZHc24eZi2GIJcJQVxtL6NT3g+mTHRNeTVfXWELzeUOirqLrh2AAyg0nfYZ/kryJWKFSCgO37DGB6Ok3qmGgEcA==", + "dependencies": { + "queue-microtask": "^1.1.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/demo/backend/store/package.json b/demo/backend/store/package.json new file mode 100644 index 00000000..ebaa3e01 --- /dev/null +++ b/demo/backend/store/package.json @@ -0,0 +1,25 @@ +{ + "name": "store-backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@types/streamify-string": "^1.0.4", + "typescript": "^5.4.5" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.19.2", + "jose": "^5.2.4", + "jwt-encode": "^1.0.1", + "n3": "^1.17.3", + "rdf-parse": "^2.3.3", + "streamify-string": "^1.0.1" + } +} diff --git a/demo/backend/store/src/createContract.ts b/demo/backend/store/src/createContract.ts new file mode 100644 index 00000000..c52abbc6 --- /dev/null +++ b/demo/backend/store/src/createContract.ts @@ -0,0 +1,30 @@ +import { Contract } from "./storage"; + +var firstDay = new Date(); +var nextWeek = new Date(firstDay.getTime() + 7 * 24 * 60 * 60 * 1000); + +const contract: Contract = { + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Agreement", + "uid": "urn:ucp:policy:120312314134", + "assigner": "http://localhost:3000/ruben/profile/card#me", + "assignee": "http://localhost:3000/store/#me", + "target": "http://localhost:3000/ruben/private/derived/age", + "permission": [{ + action: ["read", "use"], + constraint: [{ + "leftOperand": "dateTime", + "operator": "gt", + "rightOperand": { "@value": new Date().toISOString(), "@type": "xsd:date" } + }, { + "leftOperand": "dateTime", + "operator": "lt", + "rightOperand": { "@value": nextWeek.toISOString(), "@type": "xsd:date" } + }, { + "leftOperand": "purpose", + "operator": "eq", + "rightOperand": { "@id": "urn:udp:policy:constraints:age-verification" } + }] + }] +} +console.log(JSON.stringify(contract, null, 2)) \ No newline at end of file diff --git a/demo/backend/store/src/index.ts b/demo/backend/store/src/index.ts new file mode 100644 index 00000000..8e7f9e22 --- /dev/null +++ b/demo/backend/store/src/index.ts @@ -0,0 +1,132 @@ +import express, { Request, Response, response } from 'express'; +import { processAgeResult, retrieveData, terms, verifyVCsignature, verifyJwtToken } from './util' +import BackendStore, { Contract, Embedded } from './storage'; +import cors from "cors" + +const app = express() +const port = 5123 + +const serviceUrl = `http://localhost:${port}/` +const webIdDocument = serviceUrl + 'id' +const webId = webIdDocument + '#me' + + +app.use(cors()); +app.use(express.json()) +app.use((req, res, next) => { + const {method, url} = req + console.log(`[store-backend] ${method}\t${url}`) + next() +}) + +const VC_VERIFICATION_URL = "http://localhost:4444/verify" + +const storage = new BackendStore(); + +// Verification Interface + +app.get('/id', async (req,res)=>{ + + const ttlDocument = +`@prefix foaf: . +@prefix solid: . + +<${webIdDocument}> a foaf:PersonalProfileDocument; + foaf:maker <${webId}>; + foaf:primaryTopic <${webId}>. +<${webId}> a foaf:Organization; + foaf:name "The Drinks Center".` + + res.status(200) + res.contentType('text/turtle') + res.send(ttlDocument) +}) + + +app.get('/verify', async (req: Request, res: Response) => { + + let { webid } = req.query + const webId = webid as string; + console.log(`[store-backend] processing verification request for ${webId}`) + + // todo: make this take the correct webid and make the age credential to be found from the WebID? + + const credentialURL = terms.views['age-credential'] // todo: fix this + + // 1 negotiate access to age credential + const { data, token } = await retrieveData(credentialURL, webId); + + // 2 store signed token for ag + let payload + try { + payload = await verifyJwtToken(token, webId); + } catch (e) { + const warning = 'Data unusable, as token could not be verified!' + console.warn(warning) + res.statusCode = 200; + res.send({ + "verified": false, // todo: more info & credential verification result + "message": `verification failed: ${warning}` + }) + } + + // 3 Log token, contract (in token), webId (token verification check) and data as single unit + const contract = payload.contract as Contract + const embedded: Embedded = { + contract, + token, + webId, + data, + resourceId: credentialURL, + timestamp: new Date() + } + storage.storeEmbedded(embedded) + + // todo:: check purpose checking etc double check + + // 4 verify age credential signature + // todo: signature verification currently done on VC service through API. + // this should be moved to the backend itself in due time? + let result = await verifyVCsignature(VC_VERIFICATION_URL, data) + if (!result.validationResult.valid) { + res.statusCode = 200; + res.send({ + "verified": false, // todo: more info & credential verification result + "message": "Age Credential data validation failed" + }) + } + if (!result.verificationResult.verified) { + res.statusCode = 200; + res.send({ + "verified": false, // todo: more info & credential verification result + "message": "Age Credential signature verification failed" + }) + } + + // 5 check age + const decision = await processAgeResult(data, webId) + + // 6 return decision + if (decision) { + res.statusCode = 200; + res.send({ + "verified": true, + }) + } else { + res.statusCode = 200; + res.send({ + "verified": false, // todo: more info & credential verification result + "message": "verification failed" + }) + } +}) + +app.get('/audit', (req: Request, res: Response) => { + const result = storage.getLogs(); + res.status(200) + res.send(result) +}) + +app.listen(port, () => { + console.log(`[store-backend] Store backend listening on port ${port}`) +}) diff --git a/demo/backend/store/src/storage.ts b/demo/backend/store/src/storage.ts new file mode 100644 index 00000000..f5cd24d9 --- /dev/null +++ b/demo/backend/store/src/storage.ts @@ -0,0 +1,60 @@ + +export type ODRLConstraint = { + leftOperand: any, + operator: any, + rightOperand: any, +} + +export type ODRLPermission = { + action: string[], + constraint: ODRLConstraint[] +} + +export type Embedded = { + contract: Contract, + token: string, + webId: string // todo:: this should not be required + data: Data, + timestamp: Date, + resourceId: ResourceId, +} + +export type Contract = { + "@context": string, + "@type": string, + target: string, // resourceURL + uid: string, // instantiated policy UID + assigner: string, // user WebID + assignee: string, // target WebID + permission: ODRLPermission[], +}; + + +export type Data = string +export type ResourceId = string + +export type Permission = { + resource_id: string, + resource_scopes: string[], +}; + +export type AccessToken = { + permissions: Permission[], + contractId?: string, +} + +export default class BackendStore { + + private retrievals = new Array(); + + + storeEmbedded(embedded: Embedded) { + this.retrievals.push(embedded) + } + + getLogs() { + return this.retrievals + } +} + + diff --git a/demo/backend/store/src/util.ts b/demo/backend/store/src/util.ts new file mode 100644 index 00000000..96590ca3 --- /dev/null +++ b/demo/backend/store/src/util.ts @@ -0,0 +1,318 @@ +/* eslint-disable max-len */ +import { Parser, Writer, Store, Quad } from 'n3' +import sign from 'jwt-encode' + +import rdfParser from 'rdf-parse' +import Streamify from 'streamify-string' + +import { decodeJwt, createRemoteJWKSet, jwtVerify } from "jose"; + +const UMA_DISCOVERY = '/.well-known/uma2-configuration'; + +const REQUIRED_METADATA = [ + 'issuer', + 'jwks_uri', + 'permission_endpoint', + 'introspection_endpoint', + 'resource_registration_endpoint' +]; + +const parser = new Parser(); +const writer = new Writer(); + +export const terms = { + solid: { + umaServer: 'http://www.w3.org/ns/solid/terms#umaServer', + viewIndex: 'http://www.w3.org/ns/solid/terms#viewIndex', + entry: 'http://www.w3.org/ns/solid/terms#entry', + filter: 'http://www.w3.org/ns/solid/terms#filter', + location: 'http://www.w3.org/ns/solid/terms#location', + }, + filters: { + bday: 'http://localhost:3000/catalog/public/filters/bday', + age: 'http://localhost:3000/catalog/public/filters/age', + }, + views: { + bday: 'http://localhost:3000/ruben/private/derived/bday', + age: 'http://localhost:3000/ruben/private/derived/age', + "age-credential": 'http://localhost:3000/ruben/credentials/age-credential', + }, + agents: { + ruben: 'http://localhost:3000/ruben/profile/card#me', + vendor: 'http://localhost:5123/id', + present: 'http://localhost:3000/demo/public/bday-app', + }, + scopes: { + read: 'urn:example:css:modes:read', + } +} + +async function getUmaServerForWebID(webId: string) { + let profileText; + let webIdData: Store; + try { + profileText = await (await fetch(webId, {headers: { "accept": 'text-turtle'}})).text() + webIdData = new Store(parser.parse(profileText)); + } catch (e: any) { + log(e) + throw new Error('Could not read WebID information') + } + return webIdData.getObjects(webId, terms.solid.umaServer, null)[0]?.value; +} + + +export async function retrieveData(documentURI: string, webId: string): Promise<{ data: string, token?: any }> { + + const policyContainer = 'http://localhost:3000/ruben/settings/policies/generic/'; + + log(`Access to Ruben's data is based on policies he manages through his Authz Companion app, and which are stored in <${policyContainer}>. (This is, of course, not publicly known.)`); + + const umaServer = await getUmaServerForWebID(webId) + + if (!umaServer) throw new Error('Could not request access to required data for verification'); + + const configUrl = new URL('.well-known/uma2-configuration', umaServer); + const umaConfig: any = await (await fetch(configUrl)).json(); + const tokenEndpoint: any = umaConfig.token_endpoint; + + 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}>.`); + log(`Via the Well-Known endpoint <${configUrl.href}>, we can discover the Token Endpoint <${tokenEndpoint}>.`); + + 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.`); + + const accessRequest = { + permissions: [{ + resource_id: documentURI, + resource_scopes: [ terms.scopes.read ], + }] + }; + + log(JSON.stringify(accessRequest)) + + let tokenEndpointResponse = await fetch(tokenEndpoint, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(accessRequest), + }); + + // log(JSON.stringify(await tokenEndpointResponse.text())) + + + if (tokenEndpointResponse.status === 403) { + try { + console.log('TEST1111', await tokenEndpointResponse.clone().text()) + const { ticket, required_claims }: any = await tokenEndpointResponse.json(); + console.log('TEST', ticket, required_claims) + if (!ticket || !required_claims) { // There is no negotiation + throw new Error('Notification sent. Check your companion app.') + } + + log(`Based on the policy, the UMA server requests the following claims from the agent:`); + required_claims.claim_token_format[0].forEach((format: string) => log(` - ${format}`)) + + const data = { + "http://www.w3.org/ns/odrl/2/purpose": "urn:solidlab:uma:claims:purpose:age-verification", + "urn:solidlab:uma:claims:types:webid": "http://localhost:5123/id" + } + // todo: Have a store public key and use this to sign (though it's https is it really necessary?) + const secret = ('store public key') // todo: this should be the public key + + const claim_token = sign(data, secret) + + log(`The agent gathers the necessary claims (the manner in which is out-of-scope for this demo), and sends them to the UMA server as a JWT.`) + + tokenEndpointResponse = await fetch(tokenEndpoint, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + ...accessRequest, + ticket, + claim_token_format: 'urn:solidlab:uma:claims:formats:jwt', + claim_token, + }) + }); + + } catch(e) { + log(`Without a policy allowing the access, the access is denied.`); + log(`However, the UMA server enables multiple flows in which such a policy can be added, for example by notifying the resource owner. (This is out-of-scope for this demo.)`); + throw e + } + } + + if (tokenEndpointResponse.status !== 200) { log('Access request failed despite policy...'); throw new Error("Access request failed despite policy..."); } + + log(`The UMA server checks the claims with the relevant policy, and returns the agent an access token with the requested permissions.`); + + const tokenParams: any = await tokenEndpointResponse.json(); + + // Retrieving document with access token + + const accessWithTokenResponse = await fetch(documentURI, { + headers: { 'Authorization': `${tokenParams.token_type} ${tokenParams.access_token}` } + }); + + if (accessWithTokenResponse.status !== 200) { log('Access with token failed...'); throw new Error("Access with token failed..."); } + + log(`The agent can then use this access token at the Resource Server to perform the desired action.`); + + let result = await accessWithTokenResponse.text() + + return { data: result, token: tokenParams.access_token } +} + +export async function processAgeResult(data: string, webId: string): Promise { + + // .default because of some typing errors + const parsedQuads: Quad[] = await new Promise((resolve, reject) => { + let quads: Quad[] = [] + const textStream = Streamify(data); + let quadStream = rdfParser.parse(textStream, { contentType: 'application/ld+json' }); // todo: dynamic checking + quadStream + .on('data', (quad: any) => quads.push(quad)) + .on('error', (error: any) => console.error(error)) + .on('end', () => resolve(quads)); + + }) + + const store = new Store(parsedQuads) + let age = store.getQuads(null, "http://www.w3.org/2006/vcard/ns#bday", null, null)[0]?.object.value // todo: non-mocked checking + if (age && parseInt(age) >= 18) { + console.log(`Discovered age value of ${parseInt(age)}, enabling all restricted content`) + return true + } else { + console.log('Could not discover an appropriate age value for user, keeping restricted content disabled') + return false + } +} + +/* Helper functions */ + +function isString(value: any): value is string { + return typeof value === 'string' || value instanceof String; +} + +function log(msg: string, obj?: any) { + console.log(''); + console.log(msg); + if (obj) { + console.log('\n'); + console.log(obj); + } +} + + +/** + * Token verification and contract extraction + */ + + /** + * Fetch UMA Configuration of AS + * @param {string} issuer - Base URL of the UMA AS + * @return {Promise} - UMA Configuration + */ + async function fetchUmaConfig(issuer: string): Promise { + const configUrl = issuer + UMA_DISCOVERY; + const res = await fetch(configUrl); + + if (res.status >= 400) { + throw new Error(`Unable to retrieve UMA Configuration for Authorization Server '${issuer}' from '${configUrl}'`); + } + + const configuration = await res.json(); + + const missing = REQUIRED_METADATA.filter((value) => !(value in configuration)); + if (missing.length !== 0) { + throw new Error(`The Authorization Server Metadata of '${issuer}' is missing attributes ${missing.join(', ')}`); + } + + const noString = REQUIRED_METADATA.filter((value) => !isString(configuration[value])); + if (noString.length !== 0) throw new Error( + `The Authorization Server Metadata of '${issuer}' should have string attributes ${noString.join(', ')}` + ); + + return configuration; +} + + +async function verifyTokenData(token: string, issuer: string, jwks: string): Promise { + const jwkSet = await createRemoteJWKSet(new URL(jwks)); + + const { payload } = await jwtVerify(token, jwkSet, { + issuer: issuer, + audience: 'solid', + }); + + return payload +} + +/** + * Validates & parses JWT access token + * @param {string} token - the JWT access token + * @return {UmaToken} + */ +export async function verifyJwtToken(token: string, webId: string) { + let config; + let decoded; + try { + decoded = decodeJwt(token) + const issuer = decoded.iss; + if (!issuer) + throw new Error('The JWT does not contain an "iss" parameter.'); + + let umaServer = await getUmaServerForWebID(webId) + const umaServerToCheck = umaServer.endsWith('/') ? umaServer : umaServer + '/' + const issuerToCheck = issuer.endsWith('/') ? issuer : issuer + '/' + // todo: Make sure that the uma server URL is consistent everywhere in ending with a '/' or not! + if ( umaServerToCheck !== issuerToCheck ) + throw new Error(`The JWT wasn't issued by one of the target owners' issuers.`); + + config = await fetchUmaConfig(issuer); + } catch (error: unknown) { + const message = `Error verifying UMA access token: ${(error as Error).message}`; + console.warn(message); + throw new Error(message); + } + + return ( await verifyTokenData(token, config.issuer, config.jwks_uri) ) ; +} + +// MOVE THIS SHIT TO AN AUDITING INTERFACE + +export async function extractContractFromToken(token: string, webId: string) { + try { + const payload = await verifyJwtToken(token, webId) + const contract = payload.contract; + return { contract, verified: true} + } catch (_ignored) { + const payload = decodeJwt(token) + const contract = payload.contract; + return { contract, verified: false} + } +} + + +type VCVerificationResult = { + validationResult: { + valid: boolean + } + verificationResult: { + verified: boolean + } +} + +export async function verifyVCsignature(verificationUrl: string, data: string): Promise { + + const res = await fetch(verificationUrl, { + method: "POST", + body: data, + headers: { "Content-Type": "application/json" } + }) + + const {validationResult, verificationResult} = (await res.json()) as VCVerificationResult + + console.log('[store-backend] validationResult', validationResult) + console.log('[store-backend] verificationResult', verificationResult) + + return {validationResult, verificationResult} + +} \ No newline at end of file diff --git a/demo/backend/store/test/access.jsonld b/demo/backend/store/test/access.jsonld new file mode 100644 index 00000000..2dbed8f9 --- /dev/null +++ b/demo/backend/store/test/access.jsonld @@ -0,0 +1,5 @@ + +{ + "resourceId": "http://localhost:3000/ruben/private/derived/age", + "data": "@prefix rdfs: .\n@prefix foaf: .\n@prefix solid: .\n@prefix filters: .\n@prefix views: .\n@prefix ruben: .\n\n<> a foaf:PersonalProfileDocument;\n foaf:maker ruben:;\n foaf:primaryTopic ruben:.\n\nruben: a foaf:Person ;\n foaf:name \"Ruben Verborgh\"@en, \"Ruben Verborgh\"@nl;\n rdfs:label \"Ruben Verborgh\"@en, \"Ruben Verborgh\"@nl;\n solid:umaServer \"http://localhost:4000/uma/\" ;\n solid:oidcIssuer ;\n solid:viewIndex <#index> .\n \n<#index> a solid:ViewIndex ;\n solid:entry [\n solid:filter filters:bday ;\n solid:location views:bday\n ] ;\n solid:entry [\n solid:filter filters:age ;\n solid:location views:age\n ] ." +} \ No newline at end of file diff --git a/demo/backend/store/test/contract.jsonld b/demo/backend/store/test/contract.jsonld new file mode 100644 index 00000000..6c533427 --- /dev/null +++ b/demo/backend/store/test/contract.jsonld @@ -0,0 +1,41 @@ +{ + "@context": "http://www.w3.org/ns/odrl.jsonld", + "@type": "Agreement", + "uid": "urn:ucp:policy:120312314134", + "assigner": "http://localhost:3000/ruben/profile/card#me", + "assignee": "http://localhost:3000/store/#me", + "target": "http://localhost:3000/ruben/private/derived/age", + "permission": [ + { + "action": [ + "read", + "use" + ], + "constraint": [ + { + "leftOperand": "dateTime", + "operator": "gt", + "rightOperand": { + "@value": "2024-04-17T12:52:37.060Z", + "@type": "xsd:date" + } + }, + { + "leftOperand": "dateTime", + "operator": "lt", + "rightOperand": { + "@value": "2024-04-24T12:52:37.060Z", + "@type": "xsd:date" + } + }, + { + "leftOperand": "purpose", + "operator": "eq", + "rightOperand": { + "@id": "urn:udp:policy:constraints:age-verification" + } + } + ] + } + ] + } \ No newline at end of file diff --git a/demo/backend/store/tsconfig.json b/demo/backend/store/tsconfig.json new file mode 100644 index 00000000..bd682080 --- /dev/null +++ b/demo/backend/store/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist/", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/demo/data/.meta b/demo/data/.meta index d1372b07..e69de29b 100644 --- a/demo/data/.meta +++ b/demo/data/.meta @@ -1 +0,0 @@ - a . diff --git a/demo/data/ruben/settings/policies/.meta b/demo/data/ruben/credentials/.meta similarity index 100% rename from demo/data/ruben/settings/policies/.meta rename to demo/data/ruben/credentials/.meta diff --git a/demo/data/ruben/credentials/age-credential$.jsonld b/demo/data/ruben/credentials/age-credential$.jsonld new file mode 100644 index 00000000..2659b01b --- /dev/null +++ b/demo/data/ruben/credentials/age-credential$.jsonld @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "dc": "http://purl.org/dc/terms/" + }, + "https://w3id.org/security/data-integrity/v2" + ], + "id": "urn:gov.flanders.be:credentials:144c00a7-c7c1-4eb4-8fd6-a74e55976eec", + "type": [ + "VerifiableCredential" + ], + "issuer": "http://localhost:4444/id#me", + "issuanceDate": "2024-05-21T10:53:45Z", + "credentialSubject": { + "@id": "http://localhost:3000/ruben/profile/card", + "http://www.w3.org/2006/vcard/ns#bday": { + "@value": "1995-04-09T00:00:00.000Z", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + } + }, + "dc:description": "Age credential issued by Flemish Government", + "proof": { + "type": "DataIntegrityProof", + "created": "2024-05-21T10:53:45Z", + "verificationMethod": "http://localhost:4444/key", + "cryptosuite": "eddsa-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z4m7p3EeogvBjyCWjKb2AmHCsoZqKuX3DiVvKkojLQPmNAXjhUAPvfpzc63tRgVgRUHmfq1Dr3JMgQ7PWj9gAv1DV" + } +} \ No newline at end of file diff --git a/demo/data/ruben/profile/card$.ttl b/demo/data/ruben/profile/card$.ttl index 54f84452..d3ec8195 100644 --- a/demo/data/ruben/profile/card$.ttl +++ b/demo/data/ruben/profile/card$.ttl @@ -12,6 +12,7 @@ ruben: a foaf:Person ; foaf:name "Ruben Verborgh"@en, "Ruben Verborgh"@nl; rdfs:label "Ruben Verborgh"@en, "Ruben Verborgh"@nl; + foaf:img ; solid:umaServer "http://localhost:4000/uma/" ; solid:oidcIssuer ; solid:viewIndex <#index> . diff --git a/demo/data/ruben/settings/policies/generic/.meta b/demo/data/ruben/settings/policies/generic/.meta new file mode 100644 index 00000000..e69de29b diff --git a/demo/data/ruben/settings/policies/generic/age-policy$.ttl b/demo/data/ruben/settings/policies/generic/age-policy$.ttl new file mode 100644 index 00000000..83389716 --- /dev/null +++ b/demo/data/ruben/settings/policies/generic/age-policy$.ttl @@ -0,0 +1,19 @@ + . + . + . + . + . + . + . + . + . + . + . + "2024-05-21T11:35:40.729Z"^^ . + . + . + "2024-05-28T11:35:40.729Z"^^ . + . + . + "urn:solidlab:uma:claims:purpose:age-verification" . + "Age verification for food store" . diff --git a/demo/data/ruben/settings/policies/instantiated/.meta b/demo/data/ruben/settings/policies/instantiated/.meta new file mode 100644 index 00000000..e69de29b diff --git a/demo/flow.ts b/demo/flow.ts index 41d06353..6dde5a45 100644 --- a/demo/flow.ts +++ b/demo/flow.ts @@ -25,7 +25,7 @@ const terms = { }, agents: { ruben: 'http://localhost:3000/ruben/profile/card#me', - vendor: 'http://localhost:3000/demo/public/vendor', + vendor: 'http://localhost:5123/id', present: 'http://localhost:3000/demo/public/bday-app', }, scopes: { @@ -54,7 +54,7 @@ async function main() { log(`(1) <${views[terms.filters.bday]}> filters out his birth date, according to the <${terms.filters.bday}> filter`); log(`(2) <${views[terms.filters.age]}> derives his age, according to the <${terms.filters.bday}> filter`); - const policyContainer = 'http://localhost:3000/ruben/settings/policies/'; + const policyContainer = 'http://localhost:3000/ruben/settings/policies/generic/'; log(`Access to Ruben's data is based on policies he manages through his Authz Companion app, and which are stored in <${policyContainer}>. (This is, of course, not publicly known.)`); @@ -123,7 +123,7 @@ async function main() { // JWT (HS256; secret: "ceci n'est pas un secret") // { // "http://www.w3.org/ns/odrl/2/purpose": "urn:solidlab:uma:claims:purpose:age-verification", - // "urn:solidlab:uma:claims:types:webid": "http://localhost:3000/demo/public/vendor" + // "urn:solidlab:uma:claims:types:webid": "http://localhost:5123/id" // } const claim_token = "eyJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vd3d3LnczLm9yZy9ucy9vZHJsLzIvcHVycG9zZSI6InVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnB1cnBvc2U6YWdlLXZlcmlmaWNhdGlvbiIsInVybjpzb2xpZGxhYjp1bWE6Y2xhaW1zOnR5cGVzOndlYmlkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL2RlbW8vcHVibGljL3ZlbmRvciJ9.Px7G3zl1ZpTy1lk7ziRMvNv12Enb0uhup9kiVI6Ot3s" diff --git a/demo/package.json b/demo/package.json index 9b56caa4..4905a33a 100644 --- a/demo/package.json +++ b/demo/package.json @@ -14,10 +14,11 @@ "serve": "^14.2.1" }, "workspaces": [ - "sites/*" + "sites/*", + "backend/*" ], "scripts": { - "build:demo": "yarn workspaces foreach --include 'sites/*' -A -pi run build", - "start:demo": "yarn workspaces foreach --include 'sites/*' -A -pi run start" + "build:demo": "yarn workspaces foreach --include 'sites/*' --include 'backend/*' -A -pi -j unlimited run build", + "start:demo": "yarn workspaces foreach --include 'sites/*' --include 'backend/*' -A -pi -j unlimited run start" } } diff --git a/demo/problem/context.ttl b/demo/problem/context.ttl index c7657d4d..3d15f8a4 100644 --- a/demo/problem/context.ttl +++ b/demo/problem/context.ttl @@ -1,4 +1,4 @@ - ; + ; ; . diff --git a/demo/problem/policy1.ttl b/demo/problem/policy1.ttl index 665fde13..325276ce 100644 --- a/demo/problem/policy1.ttl +++ b/demo/problem/policy1.ttl @@ -6,7 +6,7 @@ a odrl:Permission; odrl:action odrl:read; odrl:target ; - odrl:assignee ; + odrl:assignee ; odrl:constraint , , . odrl:leftOperand odrl:dateTime; odrl:operator odrl:gt; diff --git a/demo/problem/policy2.ttl b/demo/problem/policy2.ttl index 19ff91ff..627532a6 100644 --- a/demo/problem/policy2.ttl +++ b/demo/problem/policy2.ttl @@ -6,7 +6,7 @@ a odrl:Permission; odrl:action odrl:read; odrl:target ; - odrl:assignee ; + odrl:assignee ; odrl:constraint , , . odrl:leftOperand odrl:dateTime; odrl:operator odrl:gt; diff --git a/demo/problem/policy3.ttl b/demo/problem/policy3.ttl index 9e03bab2..fb1ce3f9 100644 --- a/demo/problem/policy3.ttl +++ b/demo/problem/policy3.ttl @@ -6,7 +6,7 @@ a odrl:Permission; odrl:action odrl:read; odrl:target ; - odrl:assignee ; + odrl:assignee ; odrl:constraint , , . odrl:leftOperand odrl:dateTime; odrl:operator odrl:gt; diff --git a/demo/sites/auditingsite/package.json b/demo/sites/auditingsite/package.json new file mode 100644 index 00000000..98fd8b2d --- /dev/null +++ b/demo/sites/auditingsite/package.json @@ -0,0 +1,34 @@ +{ + "name": "@solidlab/uma-demo-auditing", + "version": "0.1.0", + "private": true, + "dependencies": { + "@comunica/query-sparql": "^2.6.9", + "@inrupt/solid-client-authn-browser": "^1.14.0", + "@mui/material": "^5.15.15", + "jose": "^5.2.4", + "n3": "^1.17.3", + "rdf-parse": "^2.3.3", + "stream": "^0.0.2", + "streamify-string": "^1.0.1", + "util": "^0.12.5", + "uuid": "^9.0.1" + }, + "scripts": { + "dev": "yarn run -T react-scripts start", + "start": "yarn run -T serve -s build -l 8203", + "build": "yarn run -T react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/demo/sites/auditingsite/public/index.html b/demo/sites/auditingsite/public/index.html new file mode 100644 index 00000000..f6f2dbe5 --- /dev/null +++ b/demo/sites/auditingsite/public/index.html @@ -0,0 +1,11 @@ + + + + + Auto Auditing Platform + + + +
+ + diff --git a/demo/sites/auditingsite/public/profile.png b/demo/sites/auditingsite/public/profile.png new file mode 100644 index 00000000..167ed062 Binary files /dev/null and b/demo/sites/auditingsite/public/profile.png differ diff --git a/demo/sites/storesite/public/store.jpg b/demo/sites/auditingsite/public/store.jpg similarity index 100% rename from demo/sites/storesite/public/store.jpg rename to demo/sites/auditingsite/public/store.jpg diff --git a/demo/sites/auditingsite/src/App.css b/demo/sites/auditingsite/src/App.css new file mode 100644 index 00000000..16820c46 --- /dev/null +++ b/demo/sites/auditingsite/src/App.css @@ -0,0 +1,125 @@ +.App { + text-align: center; + height: 100vh; + background-color: rgb(235, 235, 235); +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.selected { + background-color: lightblue !important; +} + +.grid-container { + height: 70vh; +} + +#retrievals-listing { +} + +.retrieval-resource-card { + /* height: 8.5em; */ + padding: 5px; + margin: 5px; + width: 100%; + word-wrap: break-word; + text-align: left; + margin-right: 1em; +} + +#audit-page { + height: 70vh; + padding-left: 2em; +} + +.text-display { + margin: 2em 2em 2em 0em ; + /* overflow-y: scroll; */ + text-align: left; + white-space: pre-wrap; + height: 80%; + overflow: hidden; + /* todo: fix height problem here other way. */ +} + +#contents { +} + +.left-border { + border-left: 1px solid black; +} + +#store-page { + background-color: #f6f6f6; + padding: 2em; +} + +.content-paragraph { + height: 100%; + overflow-y: scroll; + overflow-x: hidden; + word-break: break-all; + margin: 0; +} + + +.header-greeting { + z-index: 1500; + font-size: 1.2em; + margin: 0; + padding: 0; + padding-right: 1em; + position: absolute; + right: 0; + top: 0; + display: flex; + justify-content: center; +} + +.header-greeting .user-name { + font-weight: bold; +} + +.header-greeting > p { + margin-right: 1em; +} + +.header-greeting > img { + height: 2em; + width: 2em; + margin: auto; + border-radius: 1em; +} \ No newline at end of file diff --git a/demo/sites/auditingsite/src/App.tsx b/demo/sites/auditingsite/src/App.tsx new file mode 100644 index 00000000..84eb7562 --- /dev/null +++ b/demo/sites/auditingsite/src/App.tsx @@ -0,0 +1,18 @@ + +import './App.css'; + +import ClippedDrawer from "./components/Drawer"; + +export default function App() { + + return ( +
+
+

Logged in as:

+

Auditor #3

+ +
+ +
+ ) +} \ No newline at end of file diff --git a/demo/sites/auditingsite/src/components/AuditEntryPage.tsx b/demo/sites/auditingsite/src/components/AuditEntryPage.tsx new file mode 100644 index 00000000..78e586cd --- /dev/null +++ b/demo/sites/auditingsite/src/components/AuditEntryPage.tsx @@ -0,0 +1,28 @@ +import { Box, Grid, Typography } from "@mui/material"; +import { AuditEntry } from "../util/Types"; + +export default function AuditEntryPage({entry}: {entry: AuditEntry}) { + return ( +
+ {entry.resourceId} +
+ + Contract + + +

+ {JSON.stringify(entry.contract, null, 2)} +

+
+ + Data + + +

+ {entry.data} +

+
+
+
+ ) +} diff --git a/demo/sites/auditingsite/src/components/Drawer.tsx b/demo/sites/auditingsite/src/components/Drawer.tsx new file mode 100644 index 00000000..24d2f0b3 --- /dev/null +++ b/demo/sites/auditingsite/src/components/Drawer.tsx @@ -0,0 +1,87 @@ +import { useState, useEffect } from 'react'; +import Box from '@mui/material/Box'; +import Drawer from '@mui/material/Drawer'; +import AppBar from '@mui/material/AppBar'; +import CssBaseline from '@mui/material/CssBaseline'; +import Toolbar from '@mui/material/Toolbar'; +import List from '@mui/material/List'; +import Typography from '@mui/material/Typography'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import StorePage from './StorePage'; +import { Divider } from '@mui/material'; + +const drawerWidth = 250; + +// Map stores on their audits +export interface StoreInfo { + name: string, + site: string, + audit: string, + logo: string, +} + +const stores: StoreInfo[] = [{ + name: "The Drinks Center", + site: "http://localhost:8202/", + audit: "http://localhost:5123/audit", + logo: "store.jpg" +}] + +export default function ClippedDrawer() { + + const [selected, setSelected] = useState(undefined) + + return ( + + + theme.zIndex.drawer + 1 }}> + + + Auto Auditing Platform + + + + + + + + + + + + + + + {stores.map((store, index) => ( + setSelected(store)} disablePadding + className={selected && selected?.name === store.name ? "selected" : ""} > + + + + + + + + ))} + + + + + + { + selected && + } + + + ); +} \ No newline at end of file diff --git a/demo/sites/auditingsite/src/components/StorePage.tsx b/demo/sites/auditingsite/src/components/StorePage.tsx new file mode 100644 index 00000000..6d68de79 --- /dev/null +++ b/demo/sites/auditingsite/src/components/StorePage.tsx @@ -0,0 +1,130 @@ +/* eslint-disable max-len */ +import React, { useEffect, useState } from "react"; +import { StoreInfo } from "./Drawer"; +import { Card, Grid, Typography } from "@mui/material"; +import { AuditEntry } from "../util/Types"; +import AuditEntryPage from "./AuditEntryPage"; +import { verifyAuditCredentialSignature, verifyAuditTokenSignature, verifyCredentialAgeIsAdult, } from "../util/verification"; + + +export default function StorePage({ store }: { store: StoreInfo }) { + + const [auditEntries, setAuditEntries] = useState([]) + const [selectedEntry, setSelectedEntry] = useState(undefined) + + useEffect(() => { + async function fetchAuditEntries() { + const res = await fetch(store.audit) + const parsedEntries = await res.json() as any; + return parsedEntries.map((e: any) => { + e.timestamp = new Date(e.timestamp) + return e + }) + } + fetchAuditEntries().then(entries => setAuditEntries(entries)) //todo: uncomment + + }, []) + + return( +
+

{store.name}

+
+ {/* Data Retrievals */} + + +
+ + {auditEntries.map(entry => + selectedEntry === entry + ? + : + )} + +
+
+ + { + selectedEntry + ? + :
+ } + + + +
+
+ ) + + +} + + +function AuditEntryDisplay ({entry, selected, selectEntry}: + {entry: AuditEntry, selected: boolean, selectEntry: Function}) { + + const [tokenVerified, setTokenVerified] = useState(undefined) + const [vcVerified, setVCVerified] = useState(undefined) + const [ageVerified, setAgeVerified] = useState(undefined) + + useEffect(() => { + async function checkTokenSignature() { + try { + const verified = await verifyAuditTokenSignature(entry) + if (verified) setTokenVerified(true); + else setTokenVerified(false) + } catch (e) { setTokenVerified(false) } + } + async function checkVCSignature() { + try { + const verified = await verifyAuditCredentialSignature(entry) + if (verified) setVCVerified(true); + else setVCVerified(false) + } catch (e) { setVCVerified(false) } + } + async function checkAge() { + try { + const verified = await verifyCredentialAgeIsAdult(entry) + if (verified) setAgeVerified(true); + else setAgeVerified(false) + } catch (e) { setAgeVerified(false) } + } + checkTokenSignature() + checkVCSignature() + checkAge() + }, []) + + function getVerificationStatus(parameter: boolean | undefined, line: string) { + let color = 'black' + let status = 'checking ...' + + if (parameter === false){ + color = 'red' + status = 'FAILED' + } else if (parameter === true) { + color = 'green' + status = 'VERIFIED' + } + return ( + + {line}{status} + + ) + } + + return ( + { selectEntry(entry) }}> + + {entry.resourceId} + + + {entry.timestamp.toISOString()} + + {getVerificationStatus(tokenVerified, 'Contract signature: ')} + {getVerificationStatus(vcVerified, 'Credential signature: ')} + {getVerificationStatus(ageVerified, 'Age verification: ')} + + ) +} \ No newline at end of file diff --git a/demo/sites/auditingsite/src/index.css b/demo/sites/auditingsite/src/index.css new file mode 100644 index 00000000..82aa6460 --- /dev/null +++ b/demo/sites/auditingsite/src/index.css @@ -0,0 +1,116 @@ +@property --main-theme-color { + syntax: ""; + inherits: false; + initial-value: #1976d2; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; + width: 100vw; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +nav { + background-color: var(--main-theme-color); + margin: 0; + height: 3em; +} + +.rowcontainer { + display: flex; + flex-direction: row; +} + +.columncontainer { + display: flex; + flex-direction: column; +} + +.equalcolumns { + flex: 50%; +} + + +#policypage { + height: 80vh; + margin-top: 5vh; +} + +#policymanagementcontainer { + background-color: #fffbe4; + height: 200px; + width: 80vw; + margin: auto; + display: flex; + height: 100%; + overflow: hidden; + border: 1px solid black; +} + +#addPolicy { + flex: 20%; + display: flex; + align-items: center; + justify-content: center; +} + +#addPolicyButton { + height: 60%; + width: 60%; +} + +#policyList { + flex: 80%; + overflow-y: scroll; + padding: 30px; +} + +::-webkit-scrollbar { + width: 0; /* Remove scrollbar space */ + background: transparent; /* Optional: just make scrollbar invisible */ +} +/* Optional: show position indicator in red */ +::-webkit-scrollbar-thumb { + /* background: #FF0000; */ +} + +.policyentry { + height: 100px; + border: 2px solid gray; + margin-top: 10px; + background-color:skyblue; + text-align: left; + border-radius: 1em; + padding: .5em; +} + +.selectedentry { + background-color:lightblue; +} + +#PolicyDisplayScreen { + display: flex; + align-items: center; + justify-content: center; + flex: 60%; +} + + +#policyview { + width: 90%; + height: 90%; +} + +#PolicyListContainer { + flex: 40%; +} diff --git a/demo/sites/auditingsite/src/index.tsx b/demo/sites/auditingsite/src/index.tsx new file mode 100644 index 00000000..dd5b8c54 --- /dev/null +++ b/demo/sites/auditingsite/src/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +root.render( + + + +); + + diff --git a/demo/sites/auditingsite/src/util/Types.ts b/demo/sites/auditingsite/src/util/Types.ts new file mode 100644 index 00000000..c594683c --- /dev/null +++ b/demo/sites/auditingsite/src/util/Types.ts @@ -0,0 +1,37 @@ + +export type ODRLConstraint = { + left: any, + op: any, + right: any, + // leftOperand: any, + // operator: any, + // rightOperand: any, +} + +export type ODRLPermission = { + action: string | string[], + constraint: ODRLConstraint[] +} + +export type Contract = { + "@context": string, + "@type": string, + target: string, // resourceURL + uid: string, // instantiated policy UID + assigner: string, // user WebID + assignee: string, // target WebID + permission: ODRLPermission[], +}; + + +export type Data = string +export type ResourceId = string + +export type AuditEntry = { + contract: Contract, + token: string, + webId: string // todo:: this should not be required + data: Data, + timestamp: Date, + resourceId: ResourceId, +} diff --git a/demo/sites/auditingsite/src/util/Vocabularies.ts b/demo/sites/auditingsite/src/util/Vocabularies.ts new file mode 100644 index 00000000..cf0d14a6 --- /dev/null +++ b/demo/sites/auditingsite/src/util/Vocabularies.ts @@ -0,0 +1,139 @@ +import { DataFactory } from 'n3'; +import type { NamedNode } from '@rdfjs/types'; + +// shameless copy of the Community Solid Server Vocabularies +/** + * A `Record` in which each value is a concatenation of the baseUrl and its key. + */ +type ExpandedRecord = {[K in TLocal]: `${TBase}${K}` }; + +/** + * Has a base URL as `namespace` value and each key has as value the concatenation with that base URL. + */ +type ValueVocabulary = + { namespace: TBase } & ExpandedRecord; +/** + * A {@link ValueVocabulary} where the URI values are {@link NamedNode}s. + */ +type TermVocabulary = T extends ValueVocabulary ? {[K in keyof T]: NamedNode } : never; + +/** + * Contains a namespace and keys linking to the entries in this namespace. + * The `terms` field contains the same values but as {@link NamedNode} instead of string. + */ +export type Vocabulary = + ValueVocabulary & { terms: TermVocabulary> }; + +/** + * A {@link Vocabulary} where all the non-namespace fields are of unknown value. + * This is a fallback in case {@link createVocabulary} gets called with a non-strict string array. + */ +export type PartialVocabulary = + { namespace: TBase } & + Partial> & + { terms: { namespace: NamedNode } & Partial> }; + +/** + * A local name of a {@link Vocabulary}. + */ +export type VocabularyLocal = T extends Vocabulary ? TKey : never; +/** + * A URI string entry of a {@link Vocabulary}. + */ +export type VocabularyValue = T extends Vocabulary ? T[TKey] : never; +/** + * A {@link NamedNode} entry of a {@link Vocabulary}. + */ +export type VocabularyTerm = T extends Vocabulary ? T['terms'][TKey] : never; + +/** + * Creates a {@link ValueVocabulary} with the given `baseUri` as namespace and all `localNames` as entries. + */ +function createValueVocabulary(baseUri: TBase, localNames: TLocal[]): +ValueVocabulary { + const expanded: Partial> = {}; + // Expose the listed local names as properties + for (const localName of localNames) { + expanded[localName] = `${baseUri}${localName}`; + } + return { + namespace: baseUri, + ...expanded as ExpandedRecord, + }; +} + +/** + * Creates a {@link TermVocabulary} based on the provided {@link ValueVocabulary}. + */ +function createTermVocabulary(values: ValueVocabulary): +TermVocabulary> { + // Need to cast since `fromEntries` typings aren't strict enough + return Object.fromEntries( + Object.entries(values).map(([ key, value ]): [string, NamedNode] => [ key, DataFactory.namedNode(value) ]), + ) as TermVocabulary>; +} + +/** + * Creates a {@link Vocabulary} with the given `baseUri` as namespace and all `localNames` as entries. + * The values are the local names expanded from the given base URI as strings. + * The `terms` field contains all the same values but as {@link NamedNode} instead. + */ +export function createVocabulary(baseUri: TBase, ...localNames: TLocal[]): +string extends TLocal ? PartialVocabulary : Vocabulary { + const values = createValueVocabulary(baseUri, localNames); + return { + ...values, + terms: createTermVocabulary(values), + }; +} + +/** + * Creates a new {@link Vocabulary} that extends an existing one by adding new local names. + * @param vocabulary - The {@link Vocabulary} to extend. + * @param newNames - The new local names that need to be added. + */ +export function extendVocabulary( + vocabulary: Vocabulary, + ...newNames: TNew[] +): + ReturnType> { + const localNames = Object.keys(vocabulary) + .filter((key): boolean => key !== 'terms' && key !== 'namespace') as TLocal[]; + const allNames = [ ...localNames, ...newNames ]; + return createVocabulary(vocabulary.namespace, ...allNames); +} + +export const RDF = createVocabulary( + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'type', + ); + + +export const ODRL = createVocabulary( + 'http://www.w3.org/ns/odrl/2/', + 'Agreement', + 'Offer', + 'Permission', + 'action', + 'target', + 'assignee', + 'assigner', + 'constraint', + 'operator', + 'permission', + 'dateTime', + 'purpose', + 'leftOperand', + 'rightOperand', + 'gt', + 'lt', + 'eq', +) + +export const XSD = createVocabulary( + 'http://www.w3.org/2001/XMLSchema#', + 'dateTime', + 'duration', + 'integer', + 'string', + ); diff --git a/demo/sites/auditingsite/src/util/verification.ts b/demo/sites/auditingsite/src/util/verification.ts new file mode 100644 index 00000000..543c5574 --- /dev/null +++ b/demo/sites/auditingsite/src/util/verification.ts @@ -0,0 +1,245 @@ +import { AuditEntry } from "./Types"; +import { Parser, Writer, Store, Quad } from 'n3' + +import rdfParser from 'rdf-parse' +import Streamify from 'streamify-string' + +import { decodeJwt, createRemoteJWKSet, jwtVerify, compactVerify } from "jose"; + +const VC_VERIFICATION_URL = "http://localhost:4444/verify" +const UMA_DISCOVERY = '/.well-known/uma2-configuration'; + +const REQUIRED_METADATA = [ + 'issuer', + 'jwks_uri', + 'permission_endpoint', + 'introspection_endpoint', + 'resource_registration_endpoint' +]; + +export const terms = { + solid: { + umaServer: 'http://www.w3.org/ns/solid/terms#umaServer', + viewIndex: 'http://www.w3.org/ns/solid/terms#viewIndex', + entry: 'http://www.w3.org/ns/solid/terms#entry', + filter: 'http://www.w3.org/ns/solid/terms#filter', + location: 'http://www.w3.org/ns/solid/terms#location', + }, + filters: { + bday: 'http://localhost:3000/catalog/public/filters/bday', + age: 'http://localhost:3000/catalog/public/filters/age', + }, + views: { + bday: 'http://localhost:3000/ruben/private/derived/bday', + age: 'http://localhost:3000/ruben/private/derived/age', + "age-credential": 'http://localhost:3000/ruben/credentials/age-credential', + }, + agents: { + ruben: 'http://localhost:3000/ruben/profile/card#me', + vendor: 'http://localhost:5123/id', + present: 'http://localhost:3000/demo/public/bday-app', + }, + scopes: { + read: 'urn:example:css:modes:read', + } + } + +const parser = new Parser(); + +export async function verifyAuditTokenSignature(entry: AuditEntry): Promise { + const { token, webId } = entry + try { + const { payload } = await verifyJwtToken(token, webId) + } catch (e) { + console.error(`Could not verify entry: ${JSON.stringify(entry, null, 2)}, error: ${(e as any).toString()}`) + return false; + } + return true; +} + +export async function verifyAuditCredentialSignature(entry: AuditEntry) { + const { data } = entry + + const { validationResult, verificationResult } = await verifyVCsignature(data) + + if (!validationResult.valid) { + console.error(`Could not verify credential. Data validation failed for ${JSON.stringify(entry, null, 2)}`) + return false; + } + if (!verificationResult.verified) { + console.error(`Could not verify credential.\ + Signature verification failed for ${JSON.stringify(entry, null, 2)}`) + return false; + } + return true +} + +export async function verifyCredentialAgeIsAdult(entry: AuditEntry) { + return processAgeResult(entry.data) +} + + +/******************** + * HELPER FUNCTIONS * + ********************/ + + + + +/** + * Validates & parses JWT access token + * @param {string} token - the JWT access token + * @return {UmaToken} + */ +export async function verifyJwtToken(token: string, webId: string) { + let config; + let decoded; + try { + decoded = decodeJwt(token) + const issuer = decoded.iss; + if (!issuer) + throw new Error('The JWT does not contain an "iss" parameter.'); + + let umaServer = await getUmaServerForWebID(webId) + const umaServerToCheck = umaServer.endsWith('/') ? umaServer : umaServer + '/' + const issuerToCheck = issuer.endsWith('/') ? issuer : issuer + '/' + // todo: Make sure that the uma server URL is consistent everywhere in ending with a '/' or not! + if ( umaServerToCheck !== issuerToCheck ) + throw new Error(`The JWT wasn't issued by one of the target owners' issuers.`); + + config = await fetchUmaConfig(issuer); + } catch (error: unknown) { + const message = `Error verifying UMA access token: ${(error as Error).message}`; + console.warn(message); + throw new Error(message); + } + + return ( await verifyTokenData(token, config.issuer, config.jwks_uri) ) ; + } + + // MOVE THIS SHIT TO AN AUDITING INTERFACE + + export async function extractContractFromToken(token: string, webId: string) { + try { + const payload = await verifyJwtToken(token, webId) + const contract = payload.contract; + return { contract, verified: true} + } catch (_ignored) { + const payload = decodeJwt(token) + const contract = payload.contract; + return { contract, verified: false} + } + } + + + +async function getUmaServerForWebID(webId: string) { + let profileText; + let webIdData: Store; + try { + profileText = await (await fetch(webId, {headers: { "accept": 'text-turtle'}})).text() + webIdData = new Store(parser.parse(profileText)); + } catch (e: any) { + log(e) + throw new Error('Could not read WebID information') + } + return webIdData.getObjects(webId, terms.solid.umaServer, null)[0]?.value; +} + +export async function processAgeResult(data: string): Promise { + // todo: process as RDF. Had some issues with streams and browser + const ageValue = JSON.parse(data).credentialSubject["http://www.w3.org/2006/vcard/ns#bday"]["@value"]; + return calculate_age(ageValue) >= 18 + +} + +/* Helper functions */ + +function isString(value: any): value is string { + return typeof value === 'string' || value instanceof String; +} + +function log(msg: string, obj?: any) { + console.log(''); + console.log(msg); + if (obj) { + console.log('\n'); + console.log(obj); + } +} + + +/** + * Token verification and contract extraction + */ + + /** + * Fetch UMA Configuration of AS + * @param {string} issuer - Base URL of the UMA AS + * @return {Promise} - UMA Configuration + */ + async function fetchUmaConfig(issuer: string): Promise { + const configUrl = issuer + UMA_DISCOVERY; + const res = await fetch(configUrl); + + if (res.status >= 400) { + throw new Error(`Unable to retrieve UMA Configuration for Authorization Server '${issuer}' from '${configUrl}'`); + } + + const configuration = await res.json(); + + const missing = REQUIRED_METADATA.filter((value) => !(value in configuration)); + if (missing.length !== 0) { + throw new Error(`The Authorization Server Metadata of '${issuer}' is missing attributes ${missing.join(', ')}`); + } + + const noString = REQUIRED_METADATA.filter((value) => !isString(configuration[value])); + if (noString.length !== 0) throw new Error( + `The Authorization Server Metadata of '${issuer}' should have string attributes ${noString.join(', ')}` + ); + + return configuration; +} + +async function verifyTokenData(token: string, issuer: string, jwks: string): Promise { + const jwkSet = await createRemoteJWKSet(new URL(jwks)); + + const { payload } = await compactVerify(token, jwkSet); + + // todo: Can't do full check because of expiration of token. Need to check issuer and audience claims manually + // await jwtVerify(token, jwkSet, { issuer: issuer, audience: 'solid' }); + + return payload +} + +type VCVerificationResult = { + validationResult: { + valid: boolean + } + verificationResult: { + verified: boolean + } +} + +export async function verifyVCsignature(data: string): Promise { + const verificationUrl = VC_VERIFICATION_URL // todo: dynamic adding of stores etc ... + + const res = await fetch(verificationUrl, { + method: "POST", + body: data, + headers: { "Content-Type": "application/json" } + }) + + const {validationResult, verificationResult} = (await res.json()) as VCVerificationResult + + return {validationResult, verificationResult} + +} + + +function calculate_age(birthDate: string) { + const bdate = new Date(birthDate) + var diff_ms = Date.now() - bdate.getTime(); + var age_dt = new Date(diff_ms); + return Math.abs(age_dt.getUTCFullYear() - 1970); +} \ No newline at end of file diff --git a/demo/sites/storesite/tsconfig.json b/demo/sites/auditingsite/tsconfig.json similarity index 100% rename from demo/sites/storesite/tsconfig.json rename to demo/sites/auditingsite/tsconfig.json diff --git a/demo/sites/authorizationsite/package.json b/demo/sites/authorizationsite/package.json index 1193845d..88760e25 100644 --- a/demo/sites/authorizationsite/package.json +++ b/demo/sites/authorizationsite/package.json @@ -5,11 +5,18 @@ "dependencies": { "@comunica/query-sparql": "^2.6.9", "@inrupt/solid-client-authn-browser": "^1.14.0", + "@smessie/readable-web-to-node-stream": "^3.0.3", + "jwt-encode": "^1.0.1", "n3": "^1.17.3", + "rdf-parse": "^2.3.3", + "rdf-store-stream": "^2.0.1", + "saxes": "^6.0.0", + "streamify-string": "^1.0.1", "uuid": "^9.0.1" }, "scripts": { - "start": "yarn run -T serve -s build -l 5001", + "dev": "GENERATE_SOURCEMAP=false yarn run -T react-scripts start", + "start": "yarn run -T serve -s build -l 8201", "build": "yarn run -T react-scripts build" }, "browserslist": { diff --git a/demo/sites/authorizationsite/public/credentials/age-credential.jsonld b/demo/sites/authorizationsite/public/credentials/age-credential.jsonld new file mode 100644 index 00000000..2659b01b --- /dev/null +++ b/demo/sites/authorizationsite/public/credentials/age-credential.jsonld @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "dc": "http://purl.org/dc/terms/" + }, + "https://w3id.org/security/data-integrity/v2" + ], + "id": "urn:gov.flanders.be:credentials:144c00a7-c7c1-4eb4-8fd6-a74e55976eec", + "type": [ + "VerifiableCredential" + ], + "issuer": "http://localhost:4444/id#me", + "issuanceDate": "2024-05-21T10:53:45Z", + "credentialSubject": { + "@id": "http://localhost:3000/ruben/profile/card", + "http://www.w3.org/2006/vcard/ns#bday": { + "@value": "1995-04-09T00:00:00.000Z", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + } + }, + "dc:description": "Age credential issued by Flemish Government", + "proof": { + "type": "DataIntegrityProof", + "created": "2024-05-21T10:53:45Z", + "verificationMethod": "http://localhost:4444/key", + "cryptosuite": "eddsa-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z4m7p3EeogvBjyCWjKb2AmHCsoZqKuX3DiVvKkojLQPmNAXjhUAPvfpzc63tRgVgRUHmfq1Dr3JMgQ7PWj9gAv1DV" + } +} \ No newline at end of file diff --git a/demo/sites/authorizationsite/public/policies/age-policy.jsonld b/demo/sites/authorizationsite/public/policies/age-policy.jsonld new file mode 100644 index 00000000..a168b9ed --- /dev/null +++ b/demo/sites/authorizationsite/public/policies/age-policy.jsonld @@ -0,0 +1,26 @@ +{ + "@context": [ "http://www.w3.org/ns/odrl.jsonld", { "dc": "http://purl.org/dc/elements/1.1/"}], + "@id": "urn:ucp:be-gov:policy:d81b8118-af99-4ab3-b2a7-63f8477b6386", + "@type": "Offer", + "dc:description": "Policy managing access to and usage of Birthdate Credential", + "permission": { + "@id": "urn:ucp:rule:9524c3a0-909d-4849-86f4-7db51231a066", + "@type": "Permission", + "action": [ "read", "use" ], + "target": "http://localhost:3000/ruben/credentials/age-credential", + "constraint": [{ + "@id": "urn:ucp:constraint:40a1e210-fb5b-4536-9c94-802e5bfb78a2", + "leftOperand": "http://www.w3.org/ns/odrl/2/duration", + "operator": "http://www.w3.org/ns/odrl/2/eq", + "rightOperand": { + "@value": "P1M", + "@type": "http://www.w3.org/2001/XMLSchema#duration" + } + }, { + "@id": "urn:ucp:constraint:801dd12f-28ae-46a6-9936-15a3b4675882", + "leftOperand": "http://www.w3.org/ns/odrl/2/purpose", + "operator": "http://www.w3.org/ns/odrl/2/eq", + "rightOperand": { "@id": "urn:solidlab:uma:claims:purpose:age-verification" } + }] + } +} diff --git a/demo/sites/authorizationsite/public/policies/owner-can-read.jsonld b/demo/sites/authorizationsite/public/policies/owner-can-read.jsonld new file mode 100644 index 00000000..5d88ad70 --- /dev/null +++ b/demo/sites/authorizationsite/public/policies/owner-can-read.jsonld @@ -0,0 +1,14 @@ +{ + "@context": [ + "http://www.w3.org/ns/odrl.jsonld", + { "dc": "http://purl.org/dc/terms/" } + ], + "@id": "urn:ucp:policy:bfba29e0-a4a7-4e7a-acd2-e4bd8d1dcc6d", + "@type": "Agreement", + "assignee": "http://localhost:3000/ruben/profile/card#me", + "permission": { + "target": "http://localhost:3000/ruben/*", + "action": "read" + }, + "dc:description": "Owner can read all resources" +} diff --git a/demo/sites/authorizationsite/public/profile.png b/demo/sites/authorizationsite/public/profile.png new file mode 100644 index 00000000..04617f94 Binary files /dev/null and b/demo/sites/authorizationsite/public/profile.png differ diff --git a/demo/sites/authorizationsite/src/App.css b/demo/sites/authorizationsite/src/App.css index e247a5ff..90b3186e 100644 --- a/demo/sites/authorizationsite/src/App.css +++ b/demo/sites/authorizationsite/src/App.css @@ -1,5 +1,15 @@ + +@property --main-theme-color { + syntax: ""; + inherits: false; + initial-value: #6cc24a; + /* old value: rgb(108, 194, 74); */ +} + .App { text-align: center; + height: 100vh; + top: 0 } .App-logo { @@ -37,3 +47,37 @@ } } +.header-title { + margin: 0; + padding: 1em; + position: absolute; + left: 0; + top: 0; +} + +.header-greeting { + font-size: 1.2em; + margin: 0; + padding: 0; + padding-right: 1em; + position: absolute; + right: 0; + top: 0; + display: flex; + justify-content: center; +} + +.header-greeting .user-name { + font-weight: bold; +} + +.header-greeting > p { + margin-right: 1em; +} + +.header-greeting > img { + height: 2em; + width: 2em; + margin: auto; + border-radius: 1em; +} \ No newline at end of file diff --git a/demo/sites/authorizationsite/src/App.tsx b/demo/sites/authorizationsite/src/App.tsx index 236d3ee5..de95f4e1 100644 --- a/demo/sites/authorizationsite/src/App.tsx +++ b/demo/sites/authorizationsite/src/App.tsx @@ -8,6 +8,8 @@ import './App.css'; import Home from './components/Home'; import Navigate from './components/Navigate'; import SolidAuth from './components/SolidAuth' +import DataPage from "./components/CredentialsPage"; +import PolicyPage from "./components/PolicyPage"; const rubenWebID = 'http://localhost:3000/ruben/profile/card#me' @@ -22,7 +24,7 @@ export default function App() { // De checkingLogin variabele houdt bij of onze initiële // check voor login informatie is afgerond. - const [checkingLogin, setCheckingLogin] = useState(true) + const [checkingLogin, setCheckingLogin] = useState(false) // Deze functie voert uit bij het updaten van de component. useEffect(() => { @@ -32,7 +34,7 @@ export default function App() { // Deze functie gaat na of we teruggestuurd zijn // naar de huidige pagina door de Solid login pagina. - handleIncomingRedirect({ restorePreviousSession: true }) + handleIncomingRedirect({ restorePreviousSession: false }) .then((info) => { // Update de status van de component voor // de login status en de login check status @@ -46,39 +48,19 @@ export default function App() { .catch(console.error) }) - // return ( - //
- // { - // checkingLogin - // ? - //

Loading Session information ...

- // : ( - //
- // - // {loggedIn && - // - // - // - // } /> - // } /> - // - // - // } - //
- // ) - // } - //
- // ) - - return ( -
- - - - - } /> - - -
- ) + if (loggedIn) + return ( +
+ + +
+ ) + else { + return ( +
+ + {/* */} +
+ ) + } } \ No newline at end of file diff --git a/demo/sites/authorizationsite/src/components/CredentialsPage.tsx b/demo/sites/authorizationsite/src/components/CredentialsPage.tsx new file mode 100644 index 00000000..ffbd514d --- /dev/null +++ b/demo/sites/authorizationsite/src/components/CredentialsPage.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; +import { readCredentialsDirectory } from "../util/CredentialsManagement"; +import { VerifiableCredential } from "../util/Types"; +import { Session } from "@inrupt/solid-client-authn-browser"; + +export default function CredentialsPage({ + session +}: { + session: Session +}) { + + const [credentialsList, setCredentialsList] = useState([]) + const [selectedCredential, setSelectedCredential] = useState(null) + + useEffect(() => { + async function getCredentials() { + let credentials: VerifiableCredential[] = [] + try { + credentials = await readCredentialsDirectory(session); + } catch (e) { console.warn(e) } + + setCredentialsList(credentials) + } + getCredentials() + }, []) + + function renderCredential(entity: VerifiableCredential) { + return ( +
setSelectedCredential(entity)}> +

id: {entity.id}

+

{entity['dc:description']}

+
+ ) + } + + const selectedCredentialContents = selectedCredential + ? JSON.stringify(credentialsList.filter(c => c.id === selectedCredential.id)[0], null, 2) || '' + : '' + + return ( +
+
+
+ { + credentialsList.map(renderCredential) + } +
+
+
+