Skip to content

Commit

Permalink
Implement canister chosen alternative origins for principal derivation (
Browse files Browse the repository at this point in the history
#733)

This feature allows applications to move to other origins while keeping the original user principals. See the specification in docs/internet-identity-spec.adoc for more details.
  • Loading branch information
frederikrothenberger authored Jul 11, 2022
1 parent a65417a commit 9830ecc
Show file tree
Hide file tree
Showing 10 changed files with 514 additions and 34 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/canister_tests/src/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ xr-spatial-tracking=()",

assert!(Regex::new(
"^default-src 'none';\
connect-src 'self' https://ic0.app;\
connect-src 'self' https://ic0.app https://\\*\\.ic0.app;\
img-src 'self' data:;\
script-src 'sha256-[a-zA-Z0-9/=+]+' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https:;\
base-uri 'none';\
Expand Down
11 changes: 9 additions & 2 deletions src/frontend/src/flows/authenticate/fetchDelegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,24 @@ export const fetchDelegation = async (
authContext: AuthContext
): Promise<[PublicKey, Delegation]> => {
const sessionKey = Array.from(authContext.authRequest.sessionPublicKey);

// at this point, derivationOrigin is either validated or undefined
const derivationOrigin =
authContext.authRequest.derivationOrigin !== undefined
? authContext.authRequest.derivationOrigin
: authContext.requestOrigin;

const [userKey, timestamp] = await loginResult.connection.prepareDelegation(
loginResult.userNumber,
authContext.requestOrigin,
derivationOrigin,
sessionKey,
authContext.authRequest.maxTimeToLive
);

const signed_delegation = await retryGetDelegation(
loginResult.connection,
loginResult.userNumber,
authContext.requestOrigin,
derivationOrigin,
sessionKey,
timestamp
);
Expand Down
83 changes: 68 additions & 15 deletions src/frontend/src/flows/authenticate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ import waitForAuthRequest, { AuthContext } from "./postMessageInterface";
import { toggleErrorMessage } from "../../utils/errorHelper";
import { fetchDelegation } from "./fetchDelegation";
import { registerIfAllowed } from "../../utils/registerAllowedCheck";
import { validateDerivationOrigin } from "./validateDerivationOrigin";

const pageContent = (hostName: string, userNumber?: bigint) => html` <style>
const pageContent = (
hostName: string,
userNumber?: bigint,
derivationOrigin?: string
) => html` <style>
.anchorText {
font-size: 1.5rem;
}
Expand Down Expand Up @@ -109,6 +114,7 @@ const pageContent = (hostName: string, userNumber?: bigint) => html` <style>
.container {
padding: 3.5rem 1rem 2rem;
}
@media (min-width: 512px) {
.container {
padding: 3.5rem 2.5rem 2rem;
Expand All @@ -120,6 +126,9 @@ const pageContent = (hostName: string, userNumber?: bigint) => html` <style>
<h1>Internet Identity</h1>
<p>Authenticate to service:</p>
<div class="host-name highlightBox hostName">${hostName}</div>
${derivationOrigin !== undefined && derivationOrigin !== hostName
? derivationOriginSection(derivationOrigin)
: ""}
<p>Use Identity Anchor:</p>
<div class="childContainer">
Expand Down Expand Up @@ -163,6 +172,11 @@ const pageContent = (hostName: string, userNumber?: bigint) => html` <style>
</div>
${footer}`;

const derivationOriginSection = (derivationOrigin: string) => html` <p>
This service is an alias of:
</p>
<div class="host-name highlightBox hostName">${derivationOrigin}</div>`;

export interface AuthSuccess {
userNumber: bigint;
connection: IIConnection;
Expand All @@ -175,26 +189,61 @@ export interface AuthSuccess {
* Internet Identity window.
*/
export default async (): Promise<AuthSuccess> => {
return new Promise((resolve) => {
withLoader(async () => {
const authContext = await waitForAuthRequest();
if (authContext === null) {
// The user has manually navigated to "/#authorize".
window.location.hash = "";
window.location.reload();
return;
}
const userNumber = getUserNumber();
init(authContext, userNumber).then(resolve);
const authContext = await withLoader(() => waitForAuthRequest());
if (authContext === null) {
// The user has manually navigated to "/#authorize".
window.location.hash = "";
window.location.reload();
return new Promise((_resolve) => {
// never resolve, window is being reloaded
});
}

const validationResult = await withLoader(
async () =>
await validateDerivationOrigin(
authContext.requestOrigin,
authContext.authRequest.derivationOrigin
)
);
if (validationResult.result === "invalid") {
await displayError({
title: "Invalid Derivation Origin",
message: `"${authContext.authRequest.derivationOrigin}" is not a valid derivation origin for "${authContext.requestOrigin}"`,
detail: validationResult.message,
primaryButton: "Close",
});

// notify the client application
// do this after showing the error because the client application might close the window immediately after receiving the message and might not show the user what's going on
authContext.postMessageCallback({
kind: "authorize-client-failure",
text: `Invalid derivation origin: ${validationResult.message}`,
});

// we cannot recover from this, retrying or reloading won't help
// close the window as it returns the user to the offending application that opened II for authentication
window.close();
return new Promise((_resolve) => {
// never resolve, do not call init
});
}

const userNumber = getUserNumber();
return new Promise((resolve) => {
init(authContext, userNumber).then(resolve);
});
};

const init = (
authContext: AuthContext,
userNumber?: bigint
): Promise<AuthSuccess> => {
displayPage(authContext.requestOrigin, userNumber);
displayPage(
authContext.requestOrigin,
userNumber,
authContext.authRequest.derivationOrigin
);
initManagementBtn();
initRecovery();

Expand Down Expand Up @@ -324,9 +373,13 @@ const authenticateUser = async (
return init(authContext, userNumber);
};

const displayPage = (origin: string, userNumber?: bigint) => {
const displayPage = (
origin: string,
userNumber?: bigint,
derivationOrigin?: string
) => {
const container = document.getElementById("pageContent") as HTMLElement;
render(pageContent(origin, userNumber), container);
render(pageContent(origin, userNumber, derivationOrigin), container);
};

async function handleAuthSuccess(
Expand Down
19 changes: 15 additions & 4 deletions src/frontend/src/flows/authenticate/postMessageInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface AuthRequest {
kind: "authorize-client";
sessionPublicKey: Uint8Array;
maxTimeToLive?: bigint;
derivationOrigin?: string;
}

export interface Delegation {
Expand All @@ -23,6 +24,11 @@ export interface AuthResponseSuccess {
userPublicKey: Uint8Array;
}

interface AuthResponseFailure {
kind: "authorize-client-failure";
text: string;
}

/**
* All information required to process an authentication request received from
* a client application.
Expand All @@ -37,10 +43,11 @@ export interface AuthContext {
*/
requestOrigin: string;
/**
* Callback to send a result back to the sender. We currently only send
* either a success message or nothing at all.
* Callback to send a result back to the sender.
*/
postMessageCallback: (message: AuthResponseSuccess) => void;
postMessageCallback: (
message: AuthResponseSuccess | AuthResponseFailure
) => void;
}

// A message to signal that the II is ready to receive authorization requests.
Expand All @@ -64,7 +71,11 @@ export default async function waitForAuthRequest(): Promise<AuthContext | null>
window.addEventListener("message", async (event) => {
const message = event.data;
if (message.kind === "authorize-client") {
console.log("Handling authorize-client request.");
console.log(
`Handling authorize-client request ${JSON.stringify(message, (_, v) =>
typeof v === "bigint" ? v.toString() : v
)}`
);
resolve({
authRequest: message,
requestOrigin: event.origin,
Expand Down
76 changes: 76 additions & 0 deletions src/frontend/src/flows/authenticate/validateDerivationOrigin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Principal } from "@dfinity/principal";

export type ValidationResult =
| { result: "valid" }
| { result: "invalid"; message: string };

export const validateDerivationOrigin = async (
authRequestOrigin: string,
derivationOrigin?: string
): Promise<ValidationResult> => {
if (
derivationOrigin === undefined ||
derivationOrigin === authRequestOrigin
) {
// this is the default behaviour -> no further validation necessary
return { result: "valid" };
}

// check format of derivationOrigin
const matches = /^https:\/\/([\w-]*)(\.raw)?\.ic0\.app$/.exec(
derivationOrigin
);
if (matches === null) {
return {
result: "invalid",
message:
'derivationOrigin does not match regex "^https:\\/\\/([\\w-]*)(\\.raw)?\\.ic0\\.app$"',
};
}

try {
const canisterId = Principal.fromText(matches[1]); // verifies that a valid canister id was matched
const alternativeOriginsUrl = `https://${canisterId.toText()}.ic0.app/.well-known/ii-alternative-origins`;
const response = await fetch(
// always fetch non-raw
alternativeOriginsUrl,
// fail on redirects
{ redirect: "error" }
);

if (response.status !== 200) {
return {
result: "invalid",
message: `resource ${alternativeOriginsUrl} returned invalid status: ${response.status}`,
};
}

const alternativeOriginsObj = (await response.json()) as {
alternativeOrigins: string[];
};

// check for expected property
if (!Array.isArray(alternativeOriginsObj?.alternativeOrigins)) {
return {
result: "invalid",
message: `resource ${alternativeOriginsUrl} has invalid format: received ${alternativeOriginsObj}`,
};
}

// check allowed alternative origins
if (!alternativeOriginsObj.alternativeOrigins.includes(authRequestOrigin)) {
return {
result: "invalid",
message: `"${authRequestOrigin}" is not listed in the list of allowed alternative origins. Allowed alternative origins: ${alternativeOriginsObj.alternativeOrigins}`,
};
}
} catch (e) {
return {
result: "invalid",
message: `An error occurred while validation the derivationOrigin "${derivationOrigin}": ${e.message}`,
};
}

// all checks passed --> valid
return { result: "valid" };
};
Loading

0 comments on commit 9830ecc

Please sign in to comment.