-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow custom domains to use alternative origins (#2399)
* Allow custom domains to use alternative origins This PR lifts the restriction on the alternative origins feature to only allow canister id based subdomains of `ic0.app` and `icp0.io`. The canister id resolution is now the same for the session authorization flow as it is for verifiable credentials. The following restriction is still in place: the alternative origins file needs to be served from a canister accessible on the `icp0.io` domain. Allowances are made for local development using `localhost`, `0.0.0.0`, `127.0.0.1` and custom HTTP gateways. This PR also adds a unit test suite for the derivation origin validation. * Improve comment * Fix backticks * Make resolveCanisterId a default parameter
- Loading branch information
Frederik Rothenberger
authored
Apr 5, 2024
1 parent
1e1d474
commit cdf3ffd
Showing
7 changed files
with
362 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 228 additions & 0 deletions
228
src/frontend/src/utils/validateDerivationOrigin.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
import { validateDerivationOrigin } from "$src/utils/validateDerivationOrigin"; | ||
import { Principal } from "@dfinity/principal"; | ||
import { expect } from "vitest"; | ||
|
||
const FETCH_OPTS = { | ||
redirect: "error", | ||
headers: { | ||
Accept: "application/json", | ||
}, | ||
credentials: "omit", | ||
}; | ||
const TEST_CANISTER_ID = Principal.fromText("bkyz2-fmaaa-aaaaa-qaaaq-cai"); | ||
|
||
test("should validate no derivation origin", async () => { | ||
const result = await validateDerivationOrigin({ | ||
requestOrigin: "https://example.com", | ||
resolveCanisterId: () => Promise.reject("unused in this test"), | ||
}); | ||
expect(result).toEqual({ result: "valid" }); | ||
}); | ||
|
||
test("should validate same derivation origin", async () => { | ||
const result = await validateDerivationOrigin({ | ||
requestOrigin: "https://example.com", | ||
derivationOrigin: "https://example.com", | ||
resolveCanisterId: () => Promise.reject("unused in this test"), | ||
}); | ||
expect(result).toEqual({ result: "valid" }); | ||
}); | ||
|
||
test("should fetch alternative origins file from expected URL", async () => { | ||
const testCases = [ | ||
{ | ||
iiUrl: "https://identity.ic0.app", | ||
fetchUrl: `https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`, | ||
}, | ||
{ | ||
iiUrl: "https://identity.raw.ic0.app", | ||
fetchUrl: `https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`, | ||
}, | ||
{ | ||
iiUrl: "https://identity.icp0.io", | ||
fetchUrl: `https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`, | ||
}, | ||
{ | ||
iiUrl: "https://identity.internetcomputer.org", | ||
fetchUrl: `https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`, | ||
}, | ||
{ | ||
iiUrl: "http://222ew-7aaaa-aaaar-akaia-cai.localhost", | ||
fetchUrl: `http://localhost/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "http://222ew-7aaaa-aaaar-akaia-cai.localhost:4943", | ||
fetchUrl: `http://localhost:4943/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "https://222ew-7aaaa-aaaar-akaia-cai.localhost", | ||
fetchUrl: `https://localhost/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "http://localhost/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `http://localhost/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "https://localhost/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `https://localhost/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "http://0.0.0.0/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `http://0.0.0.0/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "https://0.0.0.0/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `https://0.0.0.0/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "https://0.0.0.0:1234/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `https://0.0.0.0:1234/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "http://127.0.0.1/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `http://127.0.0.1/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: "https://127.0.0.1/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `https://127.0.0.1/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: | ||
"https://totally-custom.com/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `https://totally-custom.com/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
{ | ||
iiUrl: | ||
"http://totally-custom.com:8080/?canisterId=222ew-7aaaa-aaaar-akaia-cai", | ||
fetchUrl: `https://totally-custom.com:8080/.well-known/ii-alternative-origins?canisterId=${TEST_CANISTER_ID}`, | ||
}, | ||
]; | ||
|
||
for (const { iiUrl, fetchUrl } of testCases) { | ||
const fetchMock = setupMocks({ | ||
iiUrl, | ||
response: Response.json({ | ||
alternativeOrigins: ["https://example.com"], | ||
}), | ||
}); | ||
|
||
const result = await validateDerivationOrigin({ | ||
requestOrigin: "https://example.com", | ||
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins | ||
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }), | ||
}); | ||
|
||
expect(result).toEqual({ result: "valid" }); | ||
expect(fetchMock).toHaveBeenLastCalledWith(fetchUrl, FETCH_OPTS); | ||
} | ||
}); | ||
|
||
test("should fetch alternative origins file using non-raw URL", async () => { | ||
const fetchMock = setupMocks({ | ||
iiUrl: "https://identity.ic0.app", | ||
response: Response.json({ | ||
alternativeOrigins: [`https://${TEST_CANISTER_ID}.raw.ic0.app`], | ||
}), | ||
}); | ||
|
||
const result = await validateDerivationOrigin({ | ||
requestOrigin: `https://${TEST_CANISTER_ID}.raw.ic0.app`, | ||
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins | ||
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }), | ||
}); | ||
|
||
expect(result).toEqual({ result: "valid" }); | ||
expect(fetchMock).toHaveBeenLastCalledWith( | ||
`https://${TEST_CANISTER_ID}.icp0.io/.well-known/ii-alternative-origins`, | ||
FETCH_OPTS | ||
); | ||
}); | ||
|
||
test("should not validate if canister id resolution fails", async () => { | ||
const result = await validateDerivationOrigin({ | ||
requestOrigin: "https://example.com", | ||
derivationOrigin: "https://derivation.com", | ||
resolveCanisterId: () => Promise.resolve("not_found"), | ||
}); | ||
expect(result.result).toBe("invalid"); | ||
}); | ||
|
||
test("should not validate if origin not allowed", async () => { | ||
setupMocks({ | ||
iiUrl: "https://identity.ic0.app", | ||
response: Response.json({ | ||
alternativeOrigins: ["https://not-example.com"], | ||
}), | ||
}); | ||
|
||
const result = await validateDerivationOrigin({ | ||
requestOrigin: "https://example.com", | ||
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins | ||
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }), | ||
}); | ||
|
||
expect(result.result).toBe("invalid"); | ||
}); | ||
|
||
test("should not validate if alternative origins file malformed", async () => { | ||
setupMocks({ | ||
iiUrl: "https://identity.ic0.app", | ||
response: Response.json({ | ||
notAlternativeOrigins: ["https://example.com"], | ||
}), | ||
}); | ||
|
||
const result = await validateDerivationOrigin({ | ||
requestOrigin: "https://example.com", | ||
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins | ||
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }), | ||
}); | ||
|
||
expect(result.result).toBe("invalid"); | ||
}); | ||
|
||
test("should not validate on alternative origins redirect", async () => { | ||
setupMocks({ | ||
iiUrl: "https://identity.ic0.app", | ||
response: Response.redirect("https://some-evil-url.com"), | ||
}); | ||
|
||
const result = await validateDerivationOrigin({ | ||
requestOrigin: "https://example.com", | ||
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins | ||
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }), | ||
}); | ||
|
||
expect(result.result).toBe("invalid"); | ||
}); | ||
|
||
test("should not validate on alternative origins error", async () => { | ||
setupMocks({ | ||
iiUrl: "https://identity.ic0.app", | ||
response: new Response(undefined, { status: 404 }), | ||
}); | ||
|
||
const result = await validateDerivationOrigin({ | ||
requestOrigin: "https://example.com", | ||
derivationOrigin: "https://some-url.com", // different from requestOrigin so that we need to fetch the alternative origins | ||
resolveCanisterId: () => Promise.resolve({ ok: TEST_CANISTER_ID }), | ||
}); | ||
|
||
expect(result.result).toBe("invalid"); | ||
}); | ||
|
||
const setupMocks = ({ | ||
iiUrl, | ||
response, | ||
}: { | ||
iiUrl: string; | ||
response: Response; | ||
}) => { | ||
// eslint-disable-next-line | ||
// @ts-ignore | ||
vi.spyOn(window, "location", "get").mockReturnValue(new URL(iiUrl)); | ||
const fetchMock = vi.fn(); | ||
global.fetch = fetchMock; | ||
fetchMock.mockReturnValueOnce(Promise.resolve(response)); | ||
return fetchMock; | ||
}; |
Oops, something went wrong.