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

Iframe webauthn postmessage #2825

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
105 changes: 105 additions & 0 deletions src/frontend/src/flows/iframeWebAuthn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { isNullish } from "@dfinity/utils";

export const WEBAUTHN_IFRAME_PATH = "#iframe/webauthn";

export const webAuthnInIframeFlow = (): Promise<never> => {
window.addEventListener("message", async (event) => {
Fixed Show fixed Hide fixed

Check warning

Code scanning / CodeQL

Missing origin verification in `postMessage` handler Medium

Postmessage handler has no origin check.

Copilot Autofix AI about 3 hours ago

To fix the problem, we need to verify the origin of incoming messages in the postMessage handler. This involves checking the event.origin property against a list of trusted origins before processing the message. If the origin is not trusted, the message should be ignored.

The best way to fix the problem without changing existing functionality is to add an origin check at the beginning of the postMessage handler. We will define a list of trusted origins and compare the event.origin against this list. If the origin is trusted, we proceed with the existing logic; otherwise, we ignore the message.

Suggested changeset 1
src/frontend/src/flows/iframeWebAuthn.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/frontend/src/flows/iframeWebAuthn.ts b/src/frontend/src/flows/iframeWebAuthn.ts
--- a/src/frontend/src/flows/iframeWebAuthn.ts
+++ b/src/frontend/src/flows/iframeWebAuthn.ts
@@ -7,2 +7,7 @@
   window.addEventListener("message", async (event) => {
+    const trustedOrigins = ['https://www.example.com']; // Add trusted origins here
+    if (!trustedOrigins.includes(event.origin)) {
+      console.warn(`Untrusted origin: ${event.origin}`);
+      return;
+    }
     console.log("Received create credential options", event.data);
@@ -19,3 +24,2 @@
     );
-    // TODO: check if origin is trusted with related origins
     window.parent.postMessage(
EOF
@@ -7,2 +7,7 @@
window.addEventListener("message", async (event) => {
const trustedOrigins = ['https://www.example.com']; // Add trusted origins here
if (!trustedOrigins.includes(event.origin)) {
console.warn(`Untrusted origin: ${event.origin}`);
return;
}
console.log("Received create credential options", event.data);
@@ -19,3 +24,2 @@
);
// TODO: check if origin is trusted with related origins
window.parent.postMessage(
Copilot is powered by AI and may make mistakes. Always verify output.
Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
// event.source?.postMessage("why does it not work?");
// window.parent.postMessage("why does it not work?", "*");
const credential = (await navigator.credentials.get(
event.data
)) as PublicKeyCredential;
console.log("Created credential public key");
console.log(
"event.source",
event.source,
event.origin,
event.source?.postMessage
);
// TODO: check if origin is trusted with related origins
window.parent.postMessage(
{
id: credential.id,
type: credential.type,
rawId: credential.rawId,
response: {
clientDataJSON: credential.response.clientDataJSON,
authenticatorData:
"authenticatorData" in credential.response
? credential.response.authenticatorData
: undefined,
signature:
"signature" in credential.response
? credential.response.signature
: undefined,
userHandle:
"userHandle" in credential.response
? credential.response.userHandle
: undefined,
},
// Exclude getClientExtensionResults()
},
"*"
);
});
// window.parent.postMessage("ready", "*");

return new Promise<never>((_) => {
/* halt */
});
};

export const webAuthnInIframe = (
options: CredentialRequestOptions
): Promise<Credential> => {
const { rpId, ...publicKey } = options.publicKey ?? {};
if (isNullish(rpId)) {
throw new Error("RP id is missing");
}
const callbackPromise = new Promise<Credential>((resolve) => {
const call = () =>
hiddenIframe.contentWindow?.postMessage(
{
...options,
publicKey,
},
`https://${rpId}`
);
setTimeout(call, 2000);
const listener = (event: MessageEvent) => {
console.log("event pre-check", event);
if (
event.source === hiddenIframe.contentWindow &&
event.origin === `https://${rpId}`
) {
console.log("event", event);
// if (event.data === "ready") {
// hiddenIframe.contentWindow?.postMessage(
// {
// ...options,
// publicKey,
// },
// `https://${rpId}`
// );
// return;
// }
console.log(event.data);
// window.removeEventListener("message", listener);
hiddenIframe.remove();
resolve({
...event.data,
getClientExtensionResults: () => ({}),
} as PublicKeyCredential);
}
};
console.log("are we sane?");
window.addEventListener("message", listener);
});
const hiddenIframe = document.createElement("iframe");
hiddenIframe.width = "500px";
hiddenIframe.height = "500px";
hiddenIframe.allow = "publickey-credentials-get";
document.body.appendChild(hiddenIframe);
hiddenIframe.src = `https://${rpId}${WEBAUTHN_IFRAME_PATH}`;
return callbackPromise;
};
9 changes: 8 additions & 1 deletion src/frontend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { handleLoginFlowResult } from "$src/components/authenticateBox";
import { callbackFlow, REDIRECT_CALLBACK_PATH } from "$src/flows/redirect";
import {
WEBAUTHN_IFRAME_PATH,
webAuthnInIframeFlow,
} from "$src/flows/iframeWebAuthn";
import { REDIRECT_CALLBACK_PATH, callbackFlow } from "$src/flows/redirect";
import { nonNullish } from "@dfinity/utils";
import { registerTentativeDevice } from "./flows/addDevice/welcomeView/registerTentativeDevice";
import { authFlowAuthorize } from "./flows/authorize";
Expand Down Expand Up @@ -47,6 +51,9 @@ void createSpa(async (connection) => {
} else if (url.pathname === REDIRECT_CALLBACK_PATH) {
// User was returned here after redirect from a OpenID flow callback
return callbackFlow();
} else if (url.hash === WEBAUTHN_IFRAME_PATH) {
// User was returned here after redirect from a OpenID flow callback
return webAuthnInIframeFlow();
} else {
// The default flow
return authFlowManage(connection);
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/utils/findWebAuthnRpId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const excludeCredentialsFromOrigins = (
rpIds: Set<string | undefined>,
currentOrigin: string
): CredentialData[] => {
// if (true || rpIds.size === 0) {
if (rpIds.size === 0) {
return credentials;
}
Expand Down Expand Up @@ -119,6 +120,7 @@ export const findWebAuthnRpId = (
): string | undefined => {
// If there are no related domains, RP ID should not be set.
if (relatedDomains.length === 0) {
// return undefined;
return undefined;
}
if (devices.length === 0) {
Expand Down
9 changes: 8 additions & 1 deletion src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,16 +423,23 @@ export class Connection {
const currentOrigin = window.location.origin;
const dynamicRPIdEnabled =
DOMAIN_COMPATIBILITY.isEnabled() &&
supportsWebauthRoR(window.navigator.userAgent);
(true ?? supportsWebauthRoR(window.navigator.userAgent));
const filteredCredentials = excludeCredentialsFromOrigins(
credentials,
cancelledRpIds,
currentOrigin
);
console.log("filteredCredentials", filteredCredentials);
// const rpId = dynamicRPIdEnabled
// ? filteredCredentials[0].origin ??
// findWebAuthnRpId(currentOrigin, filteredCredentials, relatedDomains())
// : undefined;
const rpId = dynamicRPIdEnabled
? findWebAuthnRpId(currentOrigin, filteredCredentials, relatedDomains())
: undefined;

console.log("rpId", rpId);

/* Recover the Identity (i.e. key pair) used when creating the anchor.
* If the "DUMMY_AUTH" feature is set, we use a dummy identity, the same identity
* that is used in the register flow.
Expand Down
10 changes: 9 additions & 1 deletion src/frontend/src/utils/multiWebAuthnIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* then we know which one the user is actually using
* - It doesn't support creating credentials; use `WebAuthnIdentity` for that
*/
import { webAuthnInIframe } from "$src/flows/iframeWebAuthn";
import { PublicKey, Signature, SignIdentity } from "@dfinity/agent";
import { DER_COSE_OID, unwrapDER, WebAuthnIdentity } from "@dfinity/identity";
import { isNullish } from "@dfinity/utils";
Expand Down Expand Up @@ -68,7 +69,14 @@ export class MultiWebAuthnIdentity extends SignIdentity {
return this._actualIdentity.sign(blob);
}

const result = (await navigator.credentials.get({
console.log("this.rpId", this.rpId);
const credentialsGet =
isNullish(this.rpId) || window.location.origin === this.rpId
? (options: CredentialRequestOptions) =>
navigator.credentials.get(options)
: (options: CredentialRequestOptions) => webAuthnInIframe(options);
console.log("credentialsGet", credentialsGet);
const result = (await credentialsGet({
publicKey: {
allowCredentials: this.credentialData.map((cd) => ({
type: "public-key",
Expand Down
3 changes: 2 additions & 1 deletion src/internet_identity/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ fn content_security_policy_header(integrity_hashes: Vec<String>) -> String {
style-src 'self' 'unsafe-inline';\
style-src-elem 'self' 'unsafe-inline';\
font-src 'self';\
frame-ancestors 'none';"
frame-ancestors *;\
frame-src *;"
);
// for the dev build skip upgrading all connections to II to https
#[cfg(not(feature = "dev_csp"))]
Expand Down
Loading