Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add verifiable credentials flow in frontend #2097

Merged
merged 6 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,441 changes: 1,439 additions & 2 deletions demos/test-app/package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion demos/test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"@dfinity/candid": "^0.19.3",
"@dfinity/identity": "^0.19.3",
"@dfinity/principal": "^0.19.3",
"buffer": "^6.0.3"
"buffer": "^6.0.3",
"jose": "^5.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"dev": "vite --config ./vite.config.ts",
Expand All @@ -17,6 +20,9 @@
},
"devDependencies": {
"@types/node": "^20.10.1",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.0",
"typescript": "5.2.2",
"vite": "^4.3.9"
}
Expand Down
24 changes: 11 additions & 13 deletions demos/test-app/src/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<html>
<head>
<script type="module" src="./index.ts"></script>
<script type="module" src="./index.tsx"></script>
</head>
<body>
<style>
Expand Down Expand Up @@ -28,7 +28,12 @@ <h2>Sign In</h2>
<label for="iiUrl" style="display: inline-block; width: 200px"
>Identity Provider:
</label>
<input type="text" id="iiUrl" value="https://identity.ic0.app/" />
<input
type="text"
data-role="ii-url"
id="iiUrl"
value="http://localhost:5173"
/>
</div>
<div>
<label for="maxTimeToLive" style="display: inline-block; width: 200px"
Expand All @@ -48,22 +53,14 @@ <h2>Sign In</h2>
placeholder="(use default)"
/>
</div>
<button id="signinBtn">Sign In</button>
<button data-action="authenticate" id="signinBtn">Sign In</button>
<button id="signoutBtn">Sign Out</button>
<h3>/.well-known/ii-alternative-origins</h3>
<div id="alternativeOrigins"></div>
<h3>Principal:</h3>
<div id="principal"></div>
<div data-role="principal" id="principal"></div>
<h3>Delegation:</h3>
<pre
style="
font-size: 0.8rem;
overflow-x: scroll;
white-space: pre-wrap;
word-wrap: break-word;
"
id="delegation"
></pre>
<pre id="delegation"></pre>
<h3>Expiry (ns from now):</h3>
<div id="expiration"></div>
</section>
Expand Down Expand Up @@ -136,6 +133,7 @@ <h1>Protocol Tests</h1>
<h3>WindowPostMessages:</h3>
<div id="messages"></div>
</section>
<section id="root-vc-flow"></section>
</main>
</body>
</html>
183 changes: 183 additions & 0 deletions demos/test-app/src/index.ts → demos/test-app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import {
} from "@dfinity/identity";
import { Principal } from "@dfinity/principal";

import React, { useState } from "react";
import ReactDOM from "react-dom/client";

import { decodeJwt } from "jose";

import "./main.css";

const signInBtn = document.getElementById("signinBtn") as HTMLButtonElement;
Expand Down Expand Up @@ -325,3 +330,181 @@ whoamiBtn.addEventListener("click", async () => {
console.error("Failed to fetch whoami", err);
});
});

const showError = (err: string) => {
alert(err);
};

/* The various kinds of specs the issuer canister can issue */
const credentialSpecs = {
employee: {
credentialType: "VerifiedEmployee",
arguments: { employerName: "DFINITY Foundation" },
},
grad: {
credentialType: "UniversityDegreeCredential",
arguments: { institutionName: "DFINITY College of Engineering" },
},
} as const;

type CredType = keyof typeof credentialSpecs;

/* The latest opts picked by the user. A global, because the flow is very much async */
let latestOpts:
| undefined
| {
issuerOrigin: string;
credTy: CredType;
flowId: number;
win: Window;
};

/* Callback for setting the latest received presentation. Set by React when it boots. */
let setLatestPresentation: (pres: string) => void = (_) => {
/* */
};

/* Handler for an II message telling us the window is ready */
function handleFlowReady(evnt: MessageEvent) {
if (evnt.data?.method !== "vc-flow-ready") {
return;
}

const opts = latestOpts;

if (opts === undefined) {
return showError(
"Unexpected: received OK from IDP but this test app is not ready"
);
}

const principal = principalEl.innerText;
if (principal === "") {
return showError("Principal is not set, please authenticate");
}

try {
Principal.fromText(principal) satisfies Principal;
} catch {
return showError(`"${principal}" is not a principal`);
}

// XXX: we don't export the VC-relevant types from the II codebase
// so we cannot typecheck this
const req = {
id: opts.flowId.toString(),
jsonrpc: "2.0",
method: "request_credential",
params: {
issuer: {
origin: opts.issuerOrigin,
},
credentialSpec: credentialSpecs[opts.credTy],
credentialSubject: principal,
},
};

// register a handler for the "done" message, kick start the flow and then
// unregister ourselves
try {
window.addEventListener("message", handleFlowFinished);
evnt.source?.postMessage(req, { targetOrigin: evnt.origin });
} finally {
window.removeEventListener("message", handleFlowReady);
}
}

function handleFlowFinished(evnt: MessageEvent) {
try {
// Make the presentation presentable
const verifiablePresentation = evnt.data?.result?.verifiablePresentation;
if (verifiablePresentation === undefined) {
return showError("No presentation");
}

const ver = decodeJwt(verifiablePresentation) as any;
const creds = ver.vp.verifiableCredential;
const pretty = creds.map((cred: string) =>
JSON.stringify(decodeJwt(cred), null, 2)
);

setLatestPresentation(pretty.join("\n"));
latestOpts?.win.close();
} finally {
window.removeEventListener("message", handleFlowFinished);
}
}

const App = () => {
// The URL used for connecting to the issuer
const [issuerUrl, setIssuerUrl] = useState<string>(
"http://issuer.localhost:5173"
);

// Continuously incrementing flow IDs used in the JSON RPC messages
const [nextFlowId, setNextFlowId] = useState(0);

// Latest presentation generated by a VC flow
const [latestPresentation, setLatestPresentation_] = useState<
undefined | string
>(undefined);
setLatestPresentation = setLatestPresentation_;

// Kickstart the VC flow
const startVcFlow = (credTy: CredType) => {
const urlRaw = iiUrlEl.value;

const urlParsed = new URL(urlRaw);
urlParsed.pathname = "vc-flow/";

const iiWindow = window.open(urlParsed.toString());
if (iiWindow === undefined || iiWindow === null) {
return showError("Could not open window");
}

const flowId = nextFlowId;
setNextFlowId(flowId + 1);

latestOpts = {
flowId,
credTy,
issuerOrigin: issuerUrl,
win: iiWindow,
};

window.addEventListener("message", handleFlowReady);
};

return (
<>
<h1>Verifiable Credentials</h1>
<label>
Issuer URL:
<input
data-role="issuer-url"
type="text"
value={issuerUrl}
onChange={(evt) => setIssuerUrl(evt.target.value)}
/>
</label>

<button
data-action="verify-employee"
onClick={() => startVcFlow("employee")}
>
Verify Employee Credential
</button>
<button data-action="verify-grad" onClick={() => startVcFlow("grad")}>
Verify Graduate Credential
</button>

<pre data-role="presentation">{latestPresentation}</pre>
</>
);
};

ReactDOM.createRoot(document.getElementById("root-vc-flow")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
8 changes: 8 additions & 0 deletions demos/test-app/src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ section {
main {
margin: auto;
max-width: 30rem;
margin-bottom: 5em;
}

label {
Expand Down Expand Up @@ -71,6 +72,13 @@ button {
margin-top: var(--stack);
}

pre {
font-size: 0.8rem;
overflow-x: scroll;
white-space: pre-wrap;
word-wrap: break-word;
}

/* Some specific styles */

/* Give canister logs a CRT look */
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
"watch:showcase": "astro check --root ./src/showcase --watch",
"watch": "npm run check -- --watch",
"opts": "NODE_OPTIONS='--loader ts-node/esm --experimental-specifier-resolution=node' \"$@\"",
"generate": "npm run generate:types && npm run generate:js",
"generate": "npm run generate:types && npm run generate:js && npm run generate:types-issuer && npm run generate:js-issuer",
"generate:types": "didc bind ./src/internet_identity/internet_identity.did -t ts > src/frontend/generated/internet_identity_types.d.ts",
"generate:js": "didc bind ./src/internet_identity/internet_identity.did -t js > src/frontend/generated/internet_identity_idl.js",
"generate:types-issuer": "didc bind ./demos/vc_issuer/vc_issuer.did -t ts > src/frontend/generated/vc_issuer_types.d.ts",
"generate:js-issuer": "didc bind ./demos/vc_issuer/vc_issuer.did -t js > src/frontend/generated/vc_issuer_idl.js",
"build:showcase": "tsc --noEmit && astro check --root ./src/showcase && astro build --root ./src/showcase",
"preview:showcase": "astro preview --root ./src/showcase",
"screenshots": "npm run opts -- ./src/frontend/screenshots.ts",
Expand Down Expand Up @@ -62,6 +64,7 @@
"bip39": "^3.0.4",
"buffer": "^6.0.3",
"idb-keyval": "^6.2.1",
"jose": "^5.1.3",
peterpeterparker marked this conversation as resolved.
Show resolved Hide resolved
"lit-html": "^2.7.2",
"process": "^0.11.10",
"qr-creator": "^1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/canister_tests/src/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ xr-spatial-tracking=()",
"^default-src 'none';\
connect-src 'self' https://identity.internetcomputer.org https://icp-api.io https://\\*\\.icp0.io https://\\*\\.ic0.app;\
img-src 'self' data:;\
script-src 'sha256-[a-zA-Z0-9/=+]+' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https:;\
script-src 'sha256-[a-zA-Z0-9/=+]+' 'sha256-[a-zA-Z0-9/=+]+' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https:;\
base-uri 'none';\
form-action 'none';\
style-src 'self' 'unsafe-inline';\
Expand Down
Loading
Loading