diff --git a/docs/ii-spec.md b/docs/ii-spec.md index 7a9bf33aca..0440e1b588 100644 --- a/docs/ii-spec.md +++ b/docs/ii-spec.md @@ -167,7 +167,7 @@ This section describes the Internet Identity Service from the point of view of a - the `allowPinAuthentication` (EXPERIMENTAL), if present, indicates whether or not the Identity Provider should allow the user to authenticate and/or register using a temporary key/PIN identity. Authenticating dapps may want to prevent users from using Temporary keys/PIN identities because Temporary keys/PIN identities are less secure than Passkeys (webauthn credentials) and because Temporary keys/PIN identities generally only live in a browser database (which may get cleared by the browser/OS). - - the `derivationOrigin`, if present, indicates an origin that should be used for principal derivation instead of the client origin. Values must match the following regular expression: `^https:\/\/[\w-]+(\.raw)?\.(ic0\.app|icp0\.io)$`. Internet Identity will only accept values that are also listed in the HTTP resource `https://.ic0.app/.well-known/ii-alternative-origins` of the corresponding canister (see [Alternative Frontend Origins](#alternative-frontend-origins)). + - the `derivationOrigin`, if present, indicates an origin that should be used for principal derivation instead of the client origin. Internet Identity will only accept values that are also listed in the HTTP resource `/.well-known/ii-alternative-origins` of the corresponding canister (see [Alternative Frontend Origins](#alternative-frontend-origins)). 6. Now the client application window expects a message back, with data `event`. @@ -220,12 +220,8 @@ The Internet Identity frontend will use `event.origin` as the "Frontend URL" to ## Alternative Frontend Origins - -To allow flexibility regarding the canister frontend URL, the client may choose to provide the canonical canister frontend URL (`https://.ic0.app` or `https://.raw.ic0.app`) as the `derivationOrigin` (see [Client authentication protocol](#client-authentication-protocol)). This means that Internet Identity will issue the same principals to the frontend (which uses a different origin) as it would if it were using one of the canonical URLs. - -:::note -This feature is also available for `https://.icp0.io` (resp. `https://.raw.icp0.io`). -::: +To allow flexibility regarding the canister frontend URL, the client may choose to provide another frontend URL as the `derivationOrigin` (see [Client authentication protocol](#client-authentication-protocol)). This means that Internet Identity will issue the same principals to the frontend (which uses a different origin) as it would if it were using the `derivationOrigin` directly. +This feature works for all [custom domains](https://internetcomputer.org/docs/current/developer-docs/web-apps/custom-domains/using-custom-domains) backed by canisters. :::caution This feature is intended to allow more flexibility with respect to the origins of a _single_ service. Do _not_ use this feature to allow _third party_ services to use the same principals. Only add origins you fully control to `/.well-known/ii-alternative-origins` and never set origins you do not control as `derivationOrigin`! @@ -235,11 +231,7 @@ This feature is intended to allow more flexibility with respect to the origins o `https://.ic0.app` and `https://.raw.ic0.app` do _not_ issue the same principals by default . However, this feature can also be used to map `https://.raw.ic0.app` to `https://.ic0.app` principals or vice versa. ::: -:::note -In general, Internet Identity only allows alternative origins of the form `.ic0.app` or `.icp0.io`. There is one exception: `nns.ic0.app`, which is treated as `.icp0.io`. -::: - -In order for Internet Identity to accept the `derivationOrigin` the corresponding canister must list the frontend origin in the JSON object served on the URL `https://.ic0.app/.well-known/ii-alternative-origins` (i.e. the canister _must_ implement the `http_request` query call as specified [here](https://github.com/dfinity/interface-spec/blob/master/spec/index.adoc#the-http-gateway-protocol)). +In order for Internet Identity to accept the `derivationOrigin` the corresponding canister must list the frontend origin in the JSON object served on the URL `https://.icp0.io/.well-known/ii-alternative-origins` (i.e. the canister _must_ implement the `http_request` query call as specified [here](https://github.com/dfinity/interface-spec/blob/master/spec/index.adoc#the-http-gateway-protocol)). ### JSON Schema {#alternative-frontend-origins-schema} @@ -249,7 +241,7 @@ In order for Internet Identity to accept the `derivationOrigin` the correspondin { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "II Alternative Origins Principal Derivation Origins", - "description": "An object containing the alternative frontend origins of the given canister, which are allowed to use a canonical canister URL (https://.ic0.app or https://.raw.ic0.app) for principal derivation.", + "description": "An object containing the alternative frontend origins of the given canister, which are allowed to use the URL this document is hosted on for principal derivation.", "type": "object", "properties": { "alternativeOrigins": { diff --git a/src/frontend/src/flows/authorize/index.ts b/src/frontend/src/flows/authorize/index.ts index 676e72ba3a..89fa41e587 100644 --- a/src/frontend/src/flows/authorize/index.ts +++ b/src/frontend/src/flows/authorize/index.ts @@ -167,10 +167,10 @@ const authenticate = async ( const i18n = new I18n(); const copy = i18n.i18n(copyJson); - const validationResult = await validateDerivationOrigin( - authContext.requestOrigin, - authContext.authRequest.derivationOrigin - ); + const validationResult = await validateDerivationOrigin({ + requestOrigin: authContext.requestOrigin, + derivationOrigin: authContext.authRequest.derivationOrigin, + }); if (validationResult.result === "invalid") { await displayError({ diff --git a/src/frontend/src/flows/verifiableCredentials/index.ts b/src/frontend/src/flows/verifiableCredentials/index.ts index 467623fbba..d4d121f09a 100644 --- a/src/frontend/src/flows/verifiableCredentials/index.ts +++ b/src/frontend/src/flows/verifiableCredentials/index.ts @@ -95,7 +95,10 @@ const verifyCredentials = async ({ // Verify that principals may be issued to RP using the specified // derivation origin const validRpDerivationOrigin = await withLoader(() => - validateDerivationOrigin(rpOrigin_, rpDerivationOrigin) + validateDerivationOrigin({ + requestOrigin: rpOrigin_, + derivationOrigin: rpDerivationOrigin, + }) ); if (validRpDerivationOrigin.result === "invalid") { return abortedCredentials({ reason: "bad_derivation_origin_rp" }); @@ -307,10 +310,10 @@ const getValidatedIssuerDerivationOrigin = async ({ } derivationOriginResult.kind satisfies "origin"; - const validationResult = await validateDerivationOrigin( - issuerOrigin, - derivationOriginResult.origin - ); + const validationResult = await validateDerivationOrigin({ + requestOrigin: issuerOrigin, + derivationOrigin: derivationOriginResult.origin, + }); if (validationResult.result === "invalid") { console.error( `Invalid derivation origin ${derivationOriginResult.origin} for issuer ${issuerOrigin}: ${validationResult.message}` diff --git a/src/frontend/src/test-e2e/alternativeOrigin/endpointFormat.test.ts b/src/frontend/src/test-e2e/alternativeOrigin/endpointFormat.test.ts index 7234e4bfb6..5ddae32772 100644 --- a/src/frontend/src/test-e2e/alternativeOrigin/endpointFormat.test.ts +++ b/src/frontend/src/test-e2e/alternativeOrigin/endpointFormat.test.ts @@ -136,3 +136,37 @@ test("Should fetch /.well-known/ii-alternative-origins using the non-raw url", a await niceDemoAppView.waitForAuthenticated(); }); }, 300_000); + +test("Should allow arbitrary URL as derivation origin", async () => { + await runInBrowser(async (browser: WebdriverIO.Browser) => { + const authenticatorId1 = await addVirtualAuthenticator(browser); + await browser.url(II_URL); + const userNumber = await FLOWS.registerNewIdentityWelcomeView(browser); + await FLOWS.addRecoveryMechanismSeedPhrase(browser); + const credentials = await getWebAuthnCredentials(browser, authenticatorId1); + expect(credentials).toHaveLength(1); + + const niceDemoAppView = new DemoAppView(browser); + await niceDemoAppView.open(TEST_APP_CANONICAL_URL, II_URL); + await niceDemoAppView.waitForDisplay(); + await niceDemoAppView.updateAlternativeOrigins( + `{"alternativeOrigins":["${TEST_APP_CANONICAL_URL}"]}`, + "certified" + ); + await niceDemoAppView.setDerivationOrigin(TEST_APP_NICE_URL); + expect(await niceDemoAppView.getPrincipal()).toBe(""); + await niceDemoAppView.signin(); + + const authenticatorId3 = await switchToPopup(browser); + await addWebAuthnCredential( + browser, + authenticatorId3, + credentials[0], + originToRelyingPartyId(II_URL) + ); + const authenticateView = new AuthenticateView(browser); + await authenticateView.waitForDisplay(); + await authenticateView.pickAnchor(userNumber); + await niceDemoAppView.waitForAuthenticated(); + }); +}, 300_000); diff --git a/src/frontend/src/test-e2e/alternativeOrigin/ingressFormat.test.ts b/src/frontend/src/test-e2e/alternativeOrigin/ingressFormat.test.ts index 7a42b184a4..edff76990a 100644 --- a/src/frontend/src/test-e2e/alternativeOrigin/ingressFormat.test.ts +++ b/src/frontend/src/test-e2e/alternativeOrigin/ingressFormat.test.ts @@ -49,40 +49,3 @@ test("Should not issue delegation when derivationOrigin is missing from /.well-k ); }); }, 300_000); - -test("Should not issue delegation when derivationOrigin is malformed", async () => { - await runInBrowser(async (browser: WebdriverIO.Browser) => { - const authenticatorId1 = await addVirtualAuthenticator(browser); - await browser.url(II_URL); - await FLOWS.registerNewIdentityWelcomeView(browser); - await FLOWS.addRecoveryMechanismSeedPhrase(browser); - const credentials = await getWebAuthnCredentials(browser, authenticatorId1); - expect(credentials).toHaveLength(1); - - const niceDemoAppView = new DemoAppView(browser); - await niceDemoAppView.open(TEST_APP_NICE_URL, II_URL); - await niceDemoAppView.waitForDisplay(); - await niceDemoAppView.resetAlternativeOrigins(); - await niceDemoAppView.setDerivationOrigin( - "https://some-random-disallowed-url.com" - ); - expect(await niceDemoAppView.getPrincipal()).toBe(""); - await niceDemoAppView.signin(); - - const authenticatorId3 = await switchToPopup(browser); - await addWebAuthnCredential( - browser, - authenticatorId3, - credentials[0], - originToRelyingPartyId(II_URL) - ); - const errorView = new ErrorView(browser); - await errorView.waitForDisplay(); - expect(await errorView.getErrorMessage()).toEqual( - `"https://some-random-disallowed-url.com" is not a valid derivation origin for "${TEST_APP_NICE_URL}"` - ); - expect(await errorView.getErrorDetail()).toEqual( - "derivationOrigin does not match regex /^https:\\/\\/([\\w-]+)(?:\\.raw)?\\.(?:ic0\\.app|icp0\\.io)$/" - ); - }); -}, 300_000); diff --git a/src/frontend/src/utils/validateDerivationOrigin.test.ts b/src/frontend/src/utils/validateDerivationOrigin.test.ts new file mode 100644 index 0000000000..b87aa4a06b --- /dev/null +++ b/src/frontend/src/utils/validateDerivationOrigin.test.ts @@ -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; +}; diff --git a/src/frontend/src/utils/validateDerivationOrigin.ts b/src/frontend/src/utils/validateDerivationOrigin.ts index 539f88f344..e027ac51e8 100644 --- a/src/frontend/src/utils/validateDerivationOrigin.ts +++ b/src/frontend/src/utils/validateDerivationOrigin.ts @@ -1,12 +1,8 @@ +import { resolveCanisterId as resolveCanisterIdFn } from "$src/utils/canisterIdResolution"; import { wrapError } from "$src/utils/utils"; import { Principal } from "@dfinity/principal"; import { isNullish } from "@dfinity/utils"; -// Regex that's used to ensure an alternative origin is valid. We only allow canisters as alternative origins. -// Note: this allows origins that are served both from the legacy domain (ic0.app) and the official domain (icp0.io). -const ORIGIN_VALIDATION_REGEX = - /^https:\/\/([\w-]+)(?:\.raw)?\.(?:ic0\.app|icp0\.io)$/; - const MAX_ALTERNATIVE_ORIGINS = 10; type ValidationResult = | { result: "valid" } @@ -18,57 +14,42 @@ type ValidationResult = * .well-known/ii-alternative-origins resource. * See the spec for more details: https://github.com/dfinity/internet-identity/blob/main/docs/internet-identity-spec.adoc#alternative-frontend-origins * - * This feature is currently experimental and for now limited in scope: - * - only URLs matching the {@link ORIGIN_VALIDATION_REGEX} are allowed * @param authRequestOrigin Origin of the application requesting a delegation * @param derivationOrigin Origin to use for the principal derivation for this delegation */ -export const validateDerivationOrigin = async ( - authRequestOrigin: string, - derivationOrigin?: string -): Promise => { - if (isNullish(derivationOrigin) || derivationOrigin === authRequestOrigin) { +export const validateDerivationOrigin = async ({ + requestOrigin, + derivationOrigin, + resolveCanisterId = resolveCanisterIdFn, +}: { + requestOrigin: string; + derivationOrigin?: string; + resolveCanisterId?: typeof resolveCanisterIdFn; +}): Promise => { + if (isNullish(derivationOrigin) || derivationOrigin === requestOrigin) { // this is the default behaviour -> no further validation necessary return { result: "valid" }; } - // check format of derivationOrigin - const matches = ORIGIN_VALIDATION_REGEX.exec(derivationOrigin); - if (matches === null) { - return { - result: "invalid", - message: `derivationOrigin does not match regex ${ORIGIN_VALIDATION_REGEX.toString()}`, - }; - } - try { - if (matches.length < 2) { + const canisterIdResult = await resolveCanisterId({ + origin: derivationOrigin, + }); + if (canisterIdResult === "not_found") { return { result: "invalid", - message: "invalid regex match result. No value for capture group 1.", + message: `Could not resolve canister id for derivationOrigin "${derivationOrigin}".`, }; } - const subdomain = matches[1]; + canisterIdResult satisfies { ok: Principal }; - // We only allow alternative origins of the form .(ic0.app|icp0.io), but the nns dapp - // has always been on a custom domain (nns.ic0.app). This means instead of failing because the subdomain - // is not a canister id (when the subdomain is 'nns'), we instead swap it for the nns-dapp's canister ID. - const NNS_DAPP_CANISTER_ID = Principal.fromText( - "qoctq-giaaa-aaaaa-aaaea-cai" - ); - - // verifies that a valid principal id or the nns dapp was matched - // (canister ids must be valid principal ids), throw an error (caught above) otherwise - const canisterId = - subdomain === "nns" - ? NNS_DAPP_CANISTER_ID - : Principal.fromText(subdomain); - - // Regardless of whether the _origin_ (from which principals are derived) is on ic0.app or icp0.io, we always - // query the list of alternative origins from icp0.io (official domain) - const alternativeOriginsUrl = `https://${canisterId.toText()}.icp0.io/.well-known/ii-alternative-origins`; + // We always query the list of alternative origins from a canister id based URL in order to make sure that the request + // is made through a BN that checks certification. + // Some flexibility is allowed by `inferAlternativeOriginsUrl` to allow for dev setups. + const alternativeOriginsUrl = inferAlternativeOriginsUrl({ + canisterId: canisterIdResult.ok, + }); const response = await fetch( - // always fetch non-raw alternativeOriginsUrl, // fail on redirects { @@ -111,10 +92,10 @@ export const validateDerivationOrigin = async ( } // check allowed alternative origins - if (!alternativeOriginsObj.alternativeOrigins.includes(authRequestOrigin)) { + if (!alternativeOriginsObj.alternativeOrigins.includes(requestOrigin)) { return { result: "invalid", - message: `"${authRequestOrigin}" is not listed in the list of allowed alternative origins. Allowed alternative origins: ${alternativeOriginsObj.alternativeOrigins}`, + message: `"${requestOrigin}" is not listed in the list of allowed alternative origins. Allowed alternative origins: ${alternativeOriginsObj.alternativeOrigins}`, }; } } catch (e) { @@ -129,3 +110,61 @@ export const validateDerivationOrigin = async ( // all checks passed --> valid return { result: "valid" }; }; + +/** + * Infer the URL to fetch the alternative origins file from based on the canister id + * and the current location. + * Deployments on mainnet, (including production II hosted on ic0.app or internetcomputer.org) will always only use the + * official icp0.io HTTP gateway. + * Dev deployments hosted on localhost or custom domains will use the same domain as the current location. + * + * @param canisterId The canister id to fetch the alternative origins file from. + */ +const inferAlternativeOriginsUrl = ({ + canisterId, +}: { + canisterId: Principal; +}): string => { + // The official HTTP gateway + // We never fetch from a custom domain or a raw URL in order to ensure that the request is made through a BN that checks certification. + const IC_HTTP_GATEWAY_DOMAIN = "icp0.io"; + const ALTERNATIVE_ORIGINS_PATH = "/.well-known/ii-alternative-origins"; + + const location = window?.location; + if (isNullish(location)) { + // If there is no location, then most likely this is a non-browser environment. All bets + // are off, but we return something valid just in case. + return `https://${canisterId.toText()}.${IC_HTTP_GATEWAY_DOMAIN}${ALTERNATIVE_ORIGINS_PATH}`; + } + + if ( + location.hostname.endsWith("icp0.io") || + location.hostname.endsWith("ic0.app") || + location.hostname.endsWith("internetcomputer.org") + ) { + // If this is a canister running on one of the official IC domains, then return the + // official canister id based API endpoint + return `https://${canisterId}.${IC_HTTP_GATEWAY_DOMAIN}${ALTERNATIVE_ORIGINS_PATH}`; + } + + // Local deployment -> add query parameter + // For this asset the query parameter should work regardless of whether we use a canister id based subdomain or not + if (location.hostname.endsWith("localhost")) { + // on localhost, use `localhost` as the domain to avoid routing issues in case of canister id subdomain based routing + + // preserve the port if it's not the default + const portSegment = location.port !== "" ? `:${location.port}` : ""; + + return `${location.protocol}//localhost${portSegment}${ALTERNATIVE_ORIGINS_PATH}?canisterId=${canisterId}`; + } + // Preserve host when using IP addresses + if (location.hostname === "127.0.0.1" || location.hostname === "0.0.0.0") { + return `${location.protocol}//${location.host}${ALTERNATIVE_ORIGINS_PATH}?canisterId=${canisterId}`; + } + + // Otherwise assume it's a custom setup expecting the gateway to + // - be on the same domain + // - use HTTPS + // - support query parameter based routing + return `https://${location.host}${ALTERNATIVE_ORIGINS_PATH}?canisterId=${canisterId}`; +};