Skip to content

Commit f653071

Browse files
author
Llorenç Muntaner
authored
Fix alternative origins for 2.0 (dfinity#3195)
* Fix derivation origin * Default to mainnet URL schema * Fix and add tests * Add e2e test for altnernative origins * Fix tests * Clean up comments * Move variable
1 parent 2a293d7 commit f653071

4 files changed

Lines changed: 206 additions & 94 deletions

File tree

src/frontend/src/lib/utils/validateDerivationOrigin.test.ts

Lines changed: 81 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ test("should fetch alternative origins file from expected URL", async () => {
3434
iiUrl: "https://identity.ic0.app",
3535
fetchUrl: `https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`,
3636
},
37+
{
38+
iiUrl: "https://id.ai",
39+
fetchUrl: `https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`,
40+
},
3741
{
3842
iiUrl: "https://identity.raw.ic0.app",
3943
fetchUrl: `https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`,
@@ -86,16 +90,6 @@ test("should fetch alternative origins file from expected URL", async () => {
8690
iiUrl: "https://127.0.0.1/?canisterId=222ew-7aaaa-aaaar-akaia-cai",
8791
fetchUrl: `https://127.0.0.1/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`,
8892
},
89-
{
90-
iiUrl:
91-
"https://totally-custom.com/?canisterId=222ew-7aaaa-aaaar-akaia-cai",
92-
fetchUrl: `https://totally-custom.com/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`,
93-
},
94-
{
95-
iiUrl:
96-
"http://totally-custom.com:8080/?canisterId=222ew-7aaaa-aaaar-akaia-cai",
97-
fetchUrl: `https://totally-custom.com:8080/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`,
98-
},
9993
];
10094

10195
for (const { iiUrl, fetchUrl } of testCases) {
@@ -117,27 +111,6 @@ test("should fetch alternative origins file from expected URL", async () => {
117111
}
118112
});
119113

120-
test("should fetch alternative origins file using non-raw URL", async () => {
121-
const fetchMock = setupMocks({
122-
iiUrl: "https://identity.ic0.app",
123-
response: Response.json({
124-
alternativeOrigins: [`https://${TEST_CANISTER_ID}.raw.ic0.app`],
125-
}),
126-
});
127-
128-
const result = await validateDerivationOrigin({
129-
requestOrigin: `https://${TEST_CANISTER_ID}.raw.ic0.app`,
130-
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
131-
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
132-
});
133-
134-
expect(result).toEqual({ result: "valid" });
135-
expect(fetchMock).toHaveBeenLastCalledWith(
136-
`https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`,
137-
FETCH_OPTS,
138-
);
139-
});
140-
141114
test("should not validate if canister id resolution fails", async () => {
142115
const result = await validateDerivationOrigin({
143116
requestOrigin: "https://example.com",
@@ -147,69 +120,98 @@ test("should not validate if canister id resolution fails", async () => {
147120
expect(result.result).toBe("invalid");
148121
});
149122

150-
test("should not validate if origin not allowed", async () => {
151-
setupMocks({
152-
iiUrl: "https://identity.ic0.app",
153-
response: Response.json({
154-
alternativeOrigins: ["https://not-example.com"],
155-
}),
156-
});
123+
const validIIUrls = [
124+
"https://identity.ic0.app",
125+
"https://identity.internetcomputer.org",
126+
"https://id.ai",
127+
];
157128

158-
const result = await validateDerivationOrigin({
159-
requestOrigin: "https://example.com",
160-
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
161-
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
162-
});
129+
for (const iiUrl of validIIUrls) {
130+
test("should fetch alternative origins file using non-raw URL", async () => {
131+
const fetchMock = setupMocks({
132+
iiUrl,
133+
response: Response.json({
134+
alternativeOrigins: [`https://${TEST_CANISTER_ID}.raw.ic0.app`],
135+
}),
136+
});
163137

164-
expect(result.result).toBe("invalid");
165-
});
138+
const result = await validateDerivationOrigin({
139+
requestOrigin: `https://${TEST_CANISTER_ID}.raw.ic0.app`,
140+
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
141+
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
142+
});
166143

167-
test("should not validate if alternative origins file malformed", async () => {
168-
setupMocks({
169-
iiUrl: "https://identity.ic0.app",
170-
response: Response.json({
171-
notAlternativeOrigins: ["https://example.com"],
172-
}),
144+
expect(result).toEqual({ result: "valid" });
145+
expect(fetchMock).toHaveBeenLastCalledWith(
146+
`https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`,
147+
FETCH_OPTS,
148+
);
173149
});
174150

175-
const result = await validateDerivationOrigin({
176-
requestOrigin: "https://example.com",
177-
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
178-
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
179-
});
151+
test("should not validate if origin not allowed", async () => {
152+
setupMocks({
153+
iiUrl,
154+
response: Response.json({
155+
alternativeOrigins: ["https://not-example.com"],
156+
}),
157+
});
180158

181-
expect(result.result).toBe("invalid");
182-
});
159+
const result = await validateDerivationOrigin({
160+
requestOrigin: "https://example.com",
161+
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
162+
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
163+
});
183164

184-
test("should not validate on alternative origins redirect", async () => {
185-
setupMocks({
186-
iiUrl: "https://identity.ic0.app",
187-
response: Response.redirect("https://some-evil-url.com"),
165+
expect(result.result).toBe("invalid");
188166
});
189167

190-
const result = await validateDerivationOrigin({
191-
requestOrigin: "https://example.com",
192-
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
193-
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
194-
});
168+
test("should not validate if alternative origins file malformed", async () => {
169+
setupMocks({
170+
iiUrl,
171+
response: Response.json({
172+
notAlternativeOrigins: ["https://example.com"],
173+
}),
174+
});
195175

196-
expect(result.result).toBe("invalid");
197-
});
176+
const result = await validateDerivationOrigin({
177+
requestOrigin: "https://example.com",
178+
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
179+
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
180+
});
198181

199-
test("should not validate on alternative origins error", async () => {
200-
setupMocks({
201-
iiUrl: "https://identity.ic0.app",
202-
response: new Response(undefined, { status: 404 }),
182+
expect(result.result).toBe("invalid");
203183
});
204184

205-
const result = await validateDerivationOrigin({
206-
requestOrigin: "https://example.com",
207-
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
208-
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
185+
test("should not validate on alternative origins redirect", async () => {
186+
setupMocks({
187+
iiUrl,
188+
response: Response.redirect("https://some-evil-url.com"),
189+
});
190+
191+
const result = await validateDerivationOrigin({
192+
requestOrigin: "https://example.com",
193+
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
194+
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
195+
});
196+
197+
expect(result.result).toBe("invalid");
209198
});
210199

211-
expect(result.result).toBe("invalid");
212-
});
200+
test("should not validate on alternative origins error", async () => {
201+
setupMocks({
202+
iiUrl,
203+
response: new Response(undefined, { status: 404 }),
204+
});
205+
206+
const result = await validateDerivationOrigin({
207+
requestOrigin: "https://example.com",
208+
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins
209+
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }),
210+
});
211+
212+
expect(result.result).toBe("invalid");
213+
});
214+
}
213215

214216
const setupMocks = ({
215217
iiUrl,

src/frontend/src/lib/utils/validateDerivationOrigin.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,6 @@ const inferAlternativeOriginsUrl = ({
137137
return `https://${canisterId.toText()}.${IC_HTTP_GATEWAY_DOMAIN}${ALTERNATIVE_ORIGINS_PATH}`;
138138
}
139139

140-
if (
141-
location.hostname.endsWith("icp0.io") ||
142-
location.hostname.endsWith("ic0.app") ||
143-
location.hostname.endsWith("internetcomputer.org")
144-
) {
145-
// If this is a canister running on one of the official IC domains, then return the
146-
// official canister id based API endpoint
147-
return `https://${canisterId}.${IC_HTTP_GATEWAY_DOMAIN}${ALTERNATIVE_ORIGINS_PATH}`;
148-
}
149-
150140
// Local deployment -> add query parameter
151141
// For this asset the query parameter should work regardless of whether we use a canister id based subdomain or not
152142
if (location.hostname.endsWith("localhost")) {
@@ -162,9 +152,6 @@ const inferAlternativeOriginsUrl = ({
162152
return `${location.protocol}//${location.host}${ALTERNATIVE_ORIGINS_PATH}?canisterId=${canisterId}`;
163153
}
164154

165-
// Otherwise assume it's a custom setup expecting the gateway to
166-
// - be on the same domain
167-
// - use HTTPS
168-
// - support query parameter based routing
169-
return `https://${location.host}${ALTERNATIVE_ORIGINS_PATH}?canisterId=${canisterId}`;
155+
// Otherwise, assume it's a mainnet environment.
156+
return `https://${canisterId.toText()}.${IC_HTTP_GATEWAY_DOMAIN}${ALTERNATIVE_ORIGINS_PATH}`;
170157
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { test, expect } from "@playwright/test";
2+
import {
3+
dummyAuth,
4+
II_URL,
5+
NOT_TEST_APP_URL,
6+
TEST_APP_CANONICAL_URL,
7+
TEST_APP_URL,
8+
} from "../utils";
9+
10+
test("Should not issue delegation when alternative origins are empty", async ({
11+
page,
12+
}) => {
13+
await page.goto(TEST_APP_URL);
14+
15+
// Configure the test app
16+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
17+
await page.locator("#hostUrl").fill("https://icp-api.io");
18+
await page
19+
.locator("#newAlternativeOrigins")
20+
.fill('{"alternativeOrigins":[]}');
21+
await page.locator("#certified").click();
22+
await page.locator("#updateNewAlternativeOrigins").click();
23+
24+
// Wait for alternative origins to update
25+
await expect(page.locator("#alternativeOrigins")).toHaveText(
26+
'{"alternativeOrigins":[]}',
27+
{ timeout: 6000 },
28+
);
29+
30+
// Set derivation origin
31+
await page.locator("#derivationOrigin").fill(TEST_APP_CANONICAL_URL);
32+
33+
// Attempt to sign in
34+
const pagePromise = page.context().waitForEvent("page");
35+
await page.getByRole("button", { name: "Sign In" }).click();
36+
const authPage = await pagePromise;
37+
38+
// Verify error message is displayed in II
39+
await expect(authPage.getByText("Unverified origin")).toBeVisible();
40+
});
41+
42+
test("Should not issue delegation when origin is missing from /.well-known/ii-alternative-origins", async ({
43+
page,
44+
}) => {
45+
await page.goto(TEST_APP_URL);
46+
47+
// Configure the test app
48+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
49+
await page.locator("#hostUrl").fill("https://icp-api.io");
50+
const alternativeOrigins = JSON.stringify({
51+
alternativeOrigins: [NOT_TEST_APP_URL],
52+
});
53+
await page.locator("#newAlternativeOrigins").fill(alternativeOrigins);
54+
await page.locator("#certified").click();
55+
await page.locator("#updateNewAlternativeOrigins").click();
56+
57+
// Wait for alternative origins to update
58+
await expect(page.locator("#alternativeOrigins")).toHaveText(
59+
alternativeOrigins,
60+
{ timeout: 6000 },
61+
);
62+
63+
// Set derivation origin
64+
await page.locator("#derivationOrigin").fill(TEST_APP_CANONICAL_URL);
65+
66+
// Attempt to sign in
67+
const pagePromise = page.context().waitForEvent("page");
68+
await page.getByRole("button", { name: "Sign In" }).click();
69+
const authPage = await pagePromise;
70+
71+
// Verify error message is displayed in II
72+
await expect(authPage.getByText("Unverified origin")).toBeVisible();
73+
});
74+
75+
// Add a positive test case where alternative origins are properly configured
76+
test("Should issue delegation when derivationOrigin is properly configured in /.well-known/ii-alternative-origins", async ({
77+
page,
78+
}) => {
79+
await page.goto(TEST_APP_URL);
80+
81+
// Configure the test app
82+
await page.getByRole("textbox", { name: "Identity Provider" }).fill(II_URL);
83+
const alternativeOrigins = JSON.stringify({
84+
alternativeOrigins: [TEST_APP_URL],
85+
});
86+
await page.locator("#hostUrl").fill("https://icp-api.io");
87+
await page.locator("#newAlternativeOrigins").fill(alternativeOrigins);
88+
await page.locator("#certified").click();
89+
await page.locator("#updateNewAlternativeOrigins").click();
90+
91+
// Wait for alternative origins to update
92+
await expect(page.locator("#alternativeOrigins")).toHaveText(
93+
alternativeOrigins,
94+
{ timeout: 6000 },
95+
);
96+
97+
// Set derivation origin
98+
await page.locator("#derivationOrigin").fill(TEST_APP_CANONICAL_URL);
99+
100+
// Attempt to sign in
101+
const pagePromise = page.context().waitForEvent("page");
102+
await page.getByRole("button", { name: "Sign In" }).click();
103+
const authPage = await pagePromise;
104+
105+
// Create a new identity in II
106+
await authPage.getByRole("button", { name: "Continue with Passkey" }).click();
107+
await authPage.getByRole("button", { name: "Set up a new Passkey" }).click();
108+
await authPage.getByLabel("Identity name").fill("John Doe");
109+
const auth = dummyAuth();
110+
auth(authPage);
111+
await authPage.getByRole("button", { name: "Create Passkey" }).click();
112+
113+
// Wait for II window to close
114+
await authPage.waitForEvent("close");
115+
116+
// Verify successful authentication by checking for a principal
117+
const principal = await page.locator("#principal").textContent();
118+
expect(principal).toBeTruthy();
119+
});

src/frontend/tests/e2e-playwright/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { Page, expect } from "@playwright/test";
22
import { Principal } from "@dfinity/principal";
3+
import { readCanisterId } from "@dfinity/internet-identity-vite-plugins/utils";
34

5+
const testAppCanisterId = readCanisterId({ canisterName: "test_app" });
46
export const II_URL = "https://id.ai";
57
export const TEST_APP_URL = "https://nice-name.com";
8+
export const NOT_TEST_APP_URL = "https://very-nice-name.com";
9+
export const TEST_APP_CANONICAL_URL = `https://${testAppCanisterId}.icp0.io`;
610

711
export type DummyAuthFn = (page: Page) => void;
812

0 commit comments

Comments
 (0)