Skip to content

Commit

Permalink
Make canisterId mandatory for VC flow (#2535)
Browse files Browse the repository at this point in the history
* Make canisterId mandatory for VC flow

This PR makes it mandatory for the relying party to specify
the canisterId when initiating the VC flow.

The change brings the following advantages:
* simpler logic on the II side
* support issuers with different front-end and back-end canisters without
  having them resort to alternative origins
* make the flow easier to understand for developers

* Remove left-over  from docs
  • Loading branch information
Frederik Rothenberger authored Jul 8, 2024
1 parent 1ebaeaf commit e7342f8
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 44 deletions.
18 changes: 16 additions & 2 deletions demos/test-app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ let latestOpts:
| undefined
| {
issuerOrigin: string;
issuerCanisterId: string;
derivationOrigin?: string;
credTy: CredType;
flowId: number;
Expand Down Expand Up @@ -443,14 +444,15 @@ function handleFlowReady(evnt: MessageEvent) {
params: {
issuer: {
origin: opts.issuerOrigin,
canisterId: opts.issuerCanisterId,
},
credentialSpec: credentialSpecs[opts.credTy],
credentialSubject: principal,
derivationOrigin: opts.derivationOrigin,
},
};

// register a handler for the "done" message, kick start the flow and then
// register a handler for the "done" message, kickstart the flow and then
// unregister ourselves
try {
window.addEventListener("message", handleFlowFinished);
Expand Down Expand Up @@ -497,6 +499,8 @@ const App = () => {
"http://issuer.localhost:5173"
);

const [issuerCanisterId, setIssuerCanisterId] = useState<string>("");

// Alternative origin for the RP, if any
const [derivationOrigin, setDerivationOrigin] = useState<string>("");

Expand Down Expand Up @@ -527,7 +531,8 @@ const App = () => {
latestOpts = {
flowId,
credTy,
issuerOrigin: issuerUrl,
issuerOrigin: new URL(issuerUrl).origin,
issuerCanisterId,
derivationOrigin: derivationOrigin !== "" ? derivationOrigin : undefined,
win: iiWindow,
};
Expand All @@ -547,6 +552,15 @@ const App = () => {
onChange={(evt) => setIssuerUrl(evt.target.value)}
/>
</label>
<label>
Issuer canister Id:
<input
data-role="issuer-canister-id"
type="text"
value={issuerCanisterId}
onChange={(evt) => setIssuerCanisterId(evt.target.value)}
/>
</label>
<label>
Alternative Derivation Origin:
<input
Expand Down
8 changes: 4 additions & 4 deletions docs/vc-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,8 @@ After receiving the notification that II is ready, the relying party can request
* Method: `request_credential`
* Params:
* `issuer`: An issuer that the relying party trusts. It has the following properties:
* `origin`: The origin of the issuer.
* `canisterId`: (optional) The canister id of the issuer, if applicable/known. If specified and not the same
as the one reported by a boundary node for `origin`, this is an error.
* `origin`: The front-end origin of the issuer. If this value is different from the value returned from the `derivation_origin` canister call, then the `origin` must be a valid alternative origin as per the [Alternative Frontend Origins](https://internetcomputer.org/docs/current/references/ii-spec#alternative-frontend-origins)-feature.
* `canisterId`: The canister id of the issuer canister (i.e. the one, that implements the candid issuer API as defined above).

This comment has been minimized.

Copy link
@timk11

timk11 Jul 13, 2024

Misplaced comma in this line. (Minor issue, but warrants correction in next update.)

* `credentialSpec`: The spec of the credential that the relying party wants to request from the issuer.
* `credentialType`: The type of the requested credential.
* `arguments`: (optional) A map with arguments specific to the requested credentials. It maps string keys to values that must be either strings or integers.
Expand Down Expand Up @@ -321,7 +320,8 @@ After receiving the notification that II is ready, the relying party can request
"method": "request_credential",
"params": {
"issuer": {
"origin": "https://kyc-resident-info.org"
"origin": "https://kyc-resident-info.org",
"canisterId": "rwlgt-iiaaa-aaaaa-aaaaa-cai"
},
"credentialSpec": {
"credentialType": "VerifiedResident",
Expand Down
20 changes: 1 addition & 19 deletions src/frontend/src/flows/verifiableCredentials/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { showMessage } from "$src/components/message";
import { showSpinner } from "$src/components/spinner";
import { fetchDelegation } from "$src/flows/authorize/fetchDelegation";
import { getAnchorByPrincipal } from "$src/storage";
import { resolveCanisterId } from "$src/utils/canisterIdResolution";
import { AuthenticatedConnection, Connection } from "$src/utils/iiConnection";
import { validateDerivationOrigin } from "$src/utils/validateDerivationOrigin";
import {
Expand All @@ -19,7 +18,6 @@ import {
IssuedCredentialData,
} from "@dfinity/internet-identity-vc-api";
import { Principal } from "@dfinity/principal";
import { nonNullish } from "@dfinity/utils";
import { abortedCredentials } from "./abortedCredentials";
import { allowCredentials } from "./allowCredentials";
import { VcVerifiablePresentation, vcProtocol } from "./postMessageInterface";
Expand Down Expand Up @@ -70,28 +68,12 @@ const verifyCredentials = async ({
connection,
request: {
credentialSubject: givenP_RP,
issuer: { origin: issuerOrigin, canisterId: expectedIssuerCanisterId },
issuer: { origin: issuerOrigin, canisterId: issuerCanisterId },
credentialSpec,
derivationOrigin: rpDerivationOrigin,
},
rpOrigin: rpOrigin_,
}: { connection: Connection } & VerifyCredentialsArgs) => {
// Look up the canister ID from the origin
const lookedUp = await withLoader(() =>
resolveCanisterId({ origin: issuerOrigin })
);
if (lookedUp === "not_found") {
return abortedCredentials({ reason: "no_canister_id" });
}
const issuerCanisterId = lookedUp.ok;

// If the RP provided a canister ID, check that it matches what we got
if (nonNullish(expectedIssuerCanisterId)) {
if (expectedIssuerCanisterId.compareTo(issuerCanisterId) !== "eq") {
return abortedCredentials({ reason: "bad_canister_id" });
}
}

// Verify that principals may be issued to RP using the specified
// derivation origin
const validRpDerivationOrigin = await withLoader(() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { toast } from "$src/components/toast";
import type {
import {
VcFlowReady,
VcFlowRequest,
VcResponse,
VcVerifiablePresentation,
} from "@dfinity/internet-identity-vc-api";
import { VcFlowReady, VcFlowRequest } from "@dfinity/internet-identity-vc-api";

export type { VcVerifiablePresentation } from "@dfinity/internet-identity-vc-api";

Expand Down
53 changes: 53 additions & 0 deletions src/frontend/src/test-e2e/verifiableCredentials/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ import {
KNOWN_TEST_DAPP,
TEST_APP_CANONICAL_URL,
TEST_APP_CANONICAL_URL_LEGACY,
TEST_APP_NICE_URL,
} from "$src/test-e2e/constants";

import { DemoAppView } from "$src/test-e2e/views";
import { beforeEach } from "vitest";
import {
addEmployeeToIssuer,
authenticateOnII,
authenticateToRelyingParty,
getVCPresentation,
register,
Expand Down Expand Up @@ -117,3 +122,51 @@ testConfigs.forEach(({ relyingParty, issuer, authType }) => {
300_000
);
});

test("Can issue credential with issuer front-end being hosted on a different canister", async () => {
await runInBrowser(async (browser: WebdriverIO.Browser) => {
await browser.url(II_URL);
const authConfig = await register["webauthn"](browser);
const relyingParty = TEST_APP_CANONICAL_URL;
// We pretend the issuer front-end is hosted on TEST_APP_NICE_URL
// while the relying party is TEST_APP_CANONICAL_URL.
// This is a setup where the issuer is split into two canisters, one hosting the front-end
// and one implementing the issuer canister API.
// This test demonstrates that this setup is possible _without_ configuring alternative origins,
// but simply configuring the derivation origin on the issuer canister and having the relying party specify
// the issuer canister id.
const issuer = TEST_APP_NICE_URL;
await setIssuerDerivationOrigin({
issuerCanisterId: ISSUER_CANISTER_ID,
derivationOrigin: issuer,
frontendHostname: issuer,
});

const issuerFrontEnd = new DemoAppView(browser);
await issuerFrontEnd.open(issuer, II_URL);
await issuerFrontEnd.waitForDisplay();
await issuerFrontEnd.signin();
await authenticateOnII({ authConfig, browser });
const issuerPrincipal = await issuerFrontEnd.getPrincipal();
await addEmployeeToIssuer({
issuerCanisterId: ISSUER_CANISTER_ID,
principal: issuerPrincipal,
});

// Go through the VC flow pretending the relying party URL to be the issuer front-end
const vcTestApp = await authenticateToRelyingParty({
browser,
issuer,
authConfig,
relyingParty,
});
await getVCPresentation({
vcTestApp,
browser,
authConfig,
relyingParty,
issuer,
knownDapps: [KNOWN_TEST_DAPP],
});
});
}, 300_000);
48 changes: 33 additions & 15 deletions src/frontend/src/test-e2e/verifiableCredentials/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ import {
VcTestAppView,
} from "$src/test-e2e/views";

import { II_URL, ISSUER_APP_URL, REPLICA_URL } from "$src/test-e2e/constants";
import {
II_URL,
ISSUER_APP_URL,
ISSUER_CANISTER_ID,
REPLICA_URL,
} from "$src/test-e2e/constants";

import { idlFactory as vc_issuer_idl } from "$generated/vc_issuer_idl";
import { KnownDapp } from "$src/flows/dappsExplorer/dapps";
import { Actor, ActorSubclass, HttpAgent } from "@dfinity/agent";
import { _SERVICE } from "@dfinity/internet-identity-vc-api";
import { Principal } from "@dfinity/principal";
import { nonNullish } from "@dfinity/utils";

/**
Expand Down Expand Up @@ -93,7 +99,7 @@ const authenticateWithIssuer_ = async ({
browser,
issuerAppView,
derivationOrigin,
authConfig: { setupAuth, finalizeAuth, userNumber },
authConfig,
}: {
browser: WebdriverIO.Browser;
issuerAppView: IssuerAppView;
Expand All @@ -105,6 +111,17 @@ const authenticateWithIssuer_ = async ({
}
await issuerAppView.authenticate();

await authenticateOnII({ authConfig, browser });
return issuerAppView.waitForAuthenticated();
};

export const authenticateOnII = async ({
authConfig: { setupAuth, finalizeAuth, userNumber },
browser,
}: {
authConfig: AuthConfig;
browser: WebdriverIO.Browser;
}): Promise<void> => {
await setupAuth(browser);

const authenticateView = new AuthenticateView(browser);
Expand All @@ -113,13 +130,12 @@ const authenticateWithIssuer_ = async ({

await finalizeAuth(browser);
await waitToClose(browser);
return issuerAppView.waitForAuthenticated();
};

// Open the specified test app on the URL `relyingParty` and authenticate
export const authenticateToRelyingParty = async ({
browser,
authConfig: { setupAuth, finalizeAuth, userNumber },
authConfig,
issuer,
relyingParty,
derivationOrigin,
Expand All @@ -131,7 +147,7 @@ export const authenticateToRelyingParty = async ({
derivationOrigin?: string;
}): Promise<VcTestAppView> => {
const vcTestApp = new VcTestAppView(browser);
await vcTestApp.open(relyingParty, II_URL, issuer);
await vcTestApp.open(relyingParty, II_URL, issuer, ISSUER_CANISTER_ID);

if (nonNullish(derivationOrigin)) {
const demoView = new DemoAppView(browser);
Expand All @@ -145,16 +161,7 @@ export const authenticateToRelyingParty = async ({
await vcTestApp.setAlternativeOrigin(derivationOrigin);
}
await vcTestApp.startSignIn();

await setupAuth(browser);

const authenticateView = new AuthenticateView(browser);
await authenticateView.waitForDisplay();
await authenticateView.pickAnchor(userNumber);

await finalizeAuth(browser);
await waitToClose(browser);

await authenticateOnII({ authConfig, browser });
await vcTestApp.waitForAuthenticated();

return vcTestApp;
Expand Down Expand Up @@ -318,6 +325,17 @@ export const resetIssuerOriginsConfig = async ({
await actor.set_alternative_origins('{"alternativeOrigins":[]}');
};

export const addEmployeeToIssuer = async ({
issuerCanisterId,
principal,
}: {
issuerCanisterId: string;
principal: string;
}): Promise<void> => {
const actor = await createIssuerActor(issuerCanisterId);
await actor.add_employee(Principal.from(principal));
};

const createIssuerActor = async (
issuerCanisterId: string
): Promise<ActorSubclass<_SERVICE>> => {
Expand Down
8 changes: 7 additions & 1 deletion src/frontend/src/test-e2e/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,11 +738,17 @@ export class VcTestAppView extends View {
async open(
demoAppUrl: string,
iiUrl: string,
issuerUrl: string
issuerUrl: string,
issuerCanisterId: string
): Promise<void> {
await this.browser.url(demoAppUrl);
await setInputValue(this.browser, '[data-role="ii-url"]', iiUrl);
await setInputValue(this.browser, '[data-role="issuer-url"]', issuerUrl);
await setInputValue(
this.browser,
'[data-role="issuer-canister-id"]',
issuerCanisterId
);
}

async startSignIn(): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion src/vc-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const VcFlowRequest = z.object({
origin: z
.string()
.url() /* XXX: we limit to URLs, but in practice should even be an origin */,
canisterId: z.optional(zodPrincipal),
canisterId: zodPrincipal,
}),
credentialSpec: zodCredentialSpec,
credentialSubject: zodPrincipal,
Expand Down

0 comments on commit e7342f8

Please sign in to comment.