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

Clean up test-app authentication #2236

Merged
merged 2 commits into from
Jan 26, 2024
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 change: 0 additions & 1 deletion demos/test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"private": true,
"dependencies": {
"@dfinity/agent": "*",
"@dfinity/auth-client": "*",
"@dfinity/candid": "*",
"@dfinity/identity": "*",
"@dfinity/principal": "*",
Expand Down
132 changes: 132 additions & 0 deletions demos/test-app/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { SignIdentity, Signature } from "@dfinity/agent";
import {
Delegation,
DelegationChain,
DelegationIdentity,
SignedDelegation,
} from "@dfinity/identity";
import { Principal } from "@dfinity/principal";

// The type of response from II as per the spec
interface AuthResponseSuccess {
peterpeterparker marked this conversation as resolved.
Show resolved Hide resolved
kind: "authorize-client-success";
delegations: {
delegation: {
pubkey: Uint8Array;
expiration: bigint;
targets?: Principal[];
};
signature: Uint8Array;
}[];
userPublicKey: Uint8Array;
}

// Perform a sign in to II using parameters set in this app
export const authWithII = async ({
url: url_,
maxTimeToLive,
derivationOrigin,
sessionIdentity,
}: {
url: string;
maxTimeToLive?: bigint;
derivationOrigin?: string;
sessionIdentity: SignIdentity;
}): Promise<DelegationIdentity> => {
// Figure out the II URL to use
const iiUrl = new URL(url_);
iiUrl.hash = "#authorize";

// Open an II window and kickstart the flow
const win = window.open(iiUrl, "ii-window");
if (win === null) {
throw new Error(`Could not open window for '${iiUrl}'`);
}

// Wait for II to say it's ready
const evnt = await new Promise<MessageEvent>((resolve) => {
peterpeterparker marked this conversation as resolved.
Show resolved Hide resolved
const readyHandler = (e: MessageEvent) => {
peterpeterparker marked this conversation as resolved.
Show resolved Hide resolved
window.removeEventListener("message", readyHandler);
resolve(e);
};
window.addEventListener("message", readyHandler);
});

if (evnt.data.kind !== "authorize-ready") {
throw new Error("Bad message from II window: " + JSON.stringify(evnt));
}

// Send the request to II
const sessionPublicKey: Uint8Array = new Uint8Array(
sessionIdentity.getPublicKey().toDer()
);

const request = {
kind: "authorize-client",
sessionPublicKey,
maxTimeToLive,
derivationOrigin,
};

win.postMessage(request, iiUrl.origin);

// Wait for the II response and update the local state
const response = await new Promise<MessageEvent>((resolve) => {
const responseHandler = (e: MessageEvent) => {
window.removeEventListener("message", responseHandler);
win.close();
resolve(e);
};
window.addEventListener("message", responseHandler);
});

const message = response.data;
peterpeterparker marked this conversation as resolved.
Show resolved Hide resolved
if (message.kind !== "authorize-client-success") {
throw new Error("Bad reply: " + JSON.stringify(message));
}

return identityFromResponse({
response: message as AuthResponseSuccess,
sessionIdentity,
});
};

// Read delegations the delegations from the response
const identityFromResponse = ({
sessionIdentity,
response,
}: {
sessionIdentity: SignIdentity;
response: AuthResponseSuccess;
}): DelegationIdentity => {
const delegations = response.delegations.map(extractDelegation);

const delegationChain = DelegationChain.fromDelegations(
delegations,
response.userPublicKey.buffer
);

const identity = DelegationIdentity.fromDelegation(
sessionIdentity,
delegationChain
);

return identity;
};

// Infer the type of an array's elements
type ElementOf<Arr> = Arr extends readonly (infer ElementOf)[]
? ElementOf
: "argument is not an array";

export const extractDelegation = (
signedDelegation: ElementOf<AuthResponseSuccess["delegations"]>
): SignedDelegation => ({
delegation: new Delegation(
signedDelegation.delegation.pubkey,
signedDelegation.delegation.expiration,
signedDelegation.delegation.targets
),
signature: signedDelegation.signature
.buffer as Signature /* brand type for agent-js */,
});
1 change: 0 additions & 1 deletion demos/test-app/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ <h2>Sign In</h2>
/>
</div>
<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>
Expand Down
120 changes: 63 additions & 57 deletions demos/test-app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { VcFlowRequestWire } from "@dfinity/internet-identity-vc-api";

import type { Identity, SignIdentity } from "@dfinity/agent";
import { Actor, HttpAgent } from "@dfinity/agent";
import { AuthClient } from "@dfinity/auth-client";
import {
Delegation,
DelegationChain,
DelegationIdentity,
Ed25519KeyIdentity,
Expand All @@ -16,10 +14,11 @@ import ReactDOM from "react-dom/client";

import { decodeJwt } from "jose";

import { authWithII, extractDelegation } from "./auth";

import "./main.css";

const signInBtn = document.getElementById("signinBtn") as HTMLButtonElement;
const signOutBtn = document.getElementById("signoutBtn") as HTMLButtonElement;
const whoamiBtn = document.getElementById("whoamiBtn") as HTMLButtonElement;
const updateAlternativeOriginsBtn = document.getElementById(
"updateNewAlternativeOrigins"
Expand Down Expand Up @@ -67,9 +66,19 @@ const derivationOriginEl = document.getElementById(
"derivationOrigin"
) as HTMLInputElement;

let authClient: AuthClient;
let iiProtocolTestWindow: Window | undefined;
let localIdentity: SignIdentity;

// The identity set by the authentication
let delegationIdentity: DelegationIdentity | undefined = undefined;

// The local, ephemeral key-pair
let localIdentity_: SignIdentity | undefined = undefined;
function getLocalIdentity(): SignIdentity {
if (localIdentity_ === undefined) {
localIdentity_ = Ed25519KeyIdentity.generate();
}
return localIdentity_;
}

const idlFactory = ({ IDL }: { IDL: any }) => {
const HeaderField = IDL.Tuple(IDL.Text, IDL.Text);
Expand Down Expand Up @@ -129,7 +138,13 @@ const updateAlternativeOriginsView = async () => {
alternativeOriginsEl.innerText = await response.text();
};

function addMessageElement(message: unknown, received: boolean) {
function addMessageElement({
message,
ty,
}: {
message: unknown;
ty: "received" | "sent";
}) {
const messageContainer = document.createElement("div");
messageContainer.classList.add("postMessage");
const messageTitle = document.createElement("div");
Expand All @@ -138,7 +153,7 @@ function addMessageElement(message: unknown, received: boolean) {
messageContent.innerText = JSON.stringify(message, (_, v) =>
typeof v === "bigint" ? v.toString() : v
);
if (received) {
if (ty === "received") {
messageTitle.innerText = "Message Received";
messageContainer.classList.add("received");
} else {
Expand All @@ -151,30 +166,24 @@ function addMessageElement(message: unknown, received: boolean) {
}

window.addEventListener("message", (event) => {
if (event.source === iiProtocolTestWindow) {
addMessageElement(event.data, true);
if (event?.data?.kind === "authorize-client-success") {
const delegations = event.data.delegations.map(
(signedDelegation: any) => {
return {
delegation: new Delegation(
signedDelegation.delegation.pubkey,
signedDelegation.delegation.expiration,
signedDelegation.delegation.targets
),
signature: signedDelegation.signature.buffer,
};
}
);
const delegationChain = DelegationChain.fromDelegations(
delegations,
event.data.userPublicKey.buffer
);
updateDelegationView(
DelegationIdentity.fromDelegation(localIdentity, delegationChain)
);
}
if (!event.source || event.source !== iiProtocolTestWindow) {
return;
}

addMessageElement({ message: event.data, ty: "received" });

if (event?.data?.kind !== "authorize-client-success") {
return;
}

const delegations = event.data.delegations.map(extractDelegation);
const delegationChain = DelegationChain.fromDelegations(
delegations,
event.data.userPublicKey.buffer
);
updateDelegationView(
DelegationIdentity.fromDelegation(getLocalIdentity(), delegationChain)
);
});

const readCanisterId = (): string => {
Expand All @@ -183,33 +192,30 @@ const readCanisterId = (): string => {
};

const init = async () => {
authClient = await AuthClient.create();
updateDelegationView(authClient.getIdentity());
await updateAlternativeOriginsView();
signInBtn.onclick = async () => {
let derivationOrigin =
const maxTimeToLive_ = BigInt(maxTimeToLiveEl.value);
// The default max TTL setin the @dfinity/auth-client library
const authClientDefaultMaxTTL =
/* hours */ BigInt(8) * /* nanoseconds */ BigInt(3_600_000_000_000);
const maxTimeToLive =
maxTimeToLive_ > BigInt(0) ? maxTimeToLive_ : authClientDefaultMaxTTL;
const derivationOrigin =
derivationOriginEl.value !== "" ? derivationOriginEl.value : undefined;
if (BigInt(maxTimeToLiveEl.value) > BigInt(0)) {
authClient.login({
identityProvider: iiUrlEl.value,
maxTimeToLive: BigInt(maxTimeToLiveEl.value),
derivationOrigin,
onSuccess: () => updateDelegationView(authClient.getIdentity()),
});
} else {
authClient.login({
identityProvider: iiUrlEl.value,

try {
delegationIdentity = await authWithII({
url: iiUrlEl.value,
maxTimeToLive,
derivationOrigin,
onSuccess: () => updateDelegationView(authClient.getIdentity()),
sessionIdentity: getLocalIdentity(),
});
updateDelegationView(delegationIdentity);
} catch (e) {
showError(JSON.stringify(e));
}
};

signOutBtn.onclick = async () => {
await authClient.logout();
updateDelegationView(authClient.getIdentity());
};

openIiWindowBtn.onclick = () => {
// Open a new window with the IDP provider.
if (iiProtocolTestWindow === undefined) {
Expand All @@ -231,7 +237,7 @@ const init = async () => {
return;
}
const invalidData = "some invalid data";
addMessageElement(invalidData, false);
addMessageElement({ message: invalidData, ty: "sent" });
iiProtocolTestWindow.postMessage(invalidData, iiUrlEl.value);
};

Expand All @@ -241,7 +247,7 @@ const init = async () => {
return;
}
const incompleteMessage = { kind: "authorize-client" };
addMessageElement(incompleteMessage, false);
addMessageElement({ message: incompleteMessage, ty: "sent" });
iiProtocolTestWindow.postMessage(incompleteMessage, iiUrlEl.value);
};

Expand All @@ -250,7 +256,6 @@ const init = async () => {
alert("Open II tab first");
return;
}
localIdentity = Ed25519KeyIdentity.generate();
let derivationOrigin =
derivationOriginEl.value !== "" ? derivationOriginEl.value : undefined;
let maxTimeToLive =
Expand All @@ -259,11 +264,13 @@ const init = async () => {
: undefined;
const validMessage = {
kind: "authorize-client",
sessionPublicKey: new Uint8Array(localIdentity.getPublicKey().toDer()),
sessionPublicKey: new Uint8Array(
getLocalIdentity().getPublicKey().toDer()
),
derivationOrigin,
maxTimeToLive,
};
addMessageElement(validMessage, false);
addMessageElement({ message: validMessage, ty: "sent" });
iiProtocolTestWindow.postMessage(validMessage, iiUrlEl.value);
};

Expand All @@ -273,7 +280,7 @@ const init = async () => {
return;
}
let message = JSON.parse(customMessageEl.value);
addMessageElement(message, false);
addMessageElement({ message, ty: "sent" });
iiProtocolTestWindow.postMessage(message, iiUrlEl.value);
};

Expand Down Expand Up @@ -310,12 +317,11 @@ const init = async () => {
init();

whoamiBtn.addEventListener("click", async () => {
const identity = await authClient.getIdentity();
const canisterId = Principal.fromText(readCanisterId());
const actor = Actor.createActor(idlFactory, {
agent: new HttpAgent({
host: hostUrlEl.value,
identity,
identity: delegationIdentity,
}),
canisterId,
});
Expand Down
1 change: 0 additions & 1 deletion package-lock.json

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

Loading
Loading