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

Make canisterId mandatory for VC flow #2535

Merged
merged 2 commits into from
Jul 8, 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
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).
* `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
Loading