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

implement message channel #56

Merged
merged 5 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
205 changes: 129 additions & 76 deletions auth/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ <h2>Message log</h2>
var TURNKEY_EMBEDDED_KEY = "TURNKEY_EMBEDDED_KEY";
var TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 48; // 48 hours in milliseconds

var parentFrameMessageChannelPort = null;

/**
* Creates a new public/private key pair and persists it in localStorage
*/
Expand All @@ -171,6 +173,10 @@ <h2>Message log</h2>
}
};

var setParentFrameMessageChannelPort = function (port) {
parentFrameMessageChannelPort = port;
};

/*
* Generate a key to encrypt to and export it as a JSON Web Key.
*/
Expand Down Expand Up @@ -503,13 +509,25 @@ <h2>Message log</h2>
};

/**
* Function to send a message. If this page is embedded as an iframe we'll use window.top.postMessage. Otherwise we'll display it in the DOM.
* Function to send a message.
*
* If this page is embedded as an iframe we'll send a postMessage
* in one of two ways depending on the version of @turnkey/iframe-stamper:
* 1. newer versions (>=v2.1.0) pass a MessageChannel MessagePort from the parent frame for postMessages.
* 2. older versions (<v2.1.0) still use the contentWindow so we will postMessage to the window.parent for backwards compatibility.
*
* Otherwise we'll display it in the DOM.
* @param type message type. Can be "PUBLIC_KEY_CREATED", "BUNDLE_INJECTED" or "STAMP"
* @param value message value
*/
var sendMessageUp = function (type, value) {
if (window.top !== null) {
window.top.postMessage(
if (parentFrameMessageChannelPort) {
parentFrameMessageChannelPort.postMessage({
type: type,
value: value,
})
turnekybc marked this conversation as resolved.
Show resolved Hide resolved
} else if (window.parent !== window) {
window.parent.postMessage(
{
type: type,
value: value,
Expand Down Expand Up @@ -986,6 +1004,7 @@ <h2>Message log</h2>
p256JWKPrivateToPublic,
convertEcdsaIeee1363ToDer,
sendMessageUp,
setParentFrameMessageChannelPort,
logMessage,
base64urlEncode,
base64urlDecode,
Expand All @@ -1006,6 +1025,88 @@ <h2>Message log</h2>
// In memory spot for the credential to live. We do NOT persist it to localStorage.
var CREDENTIAL_BYTES = null;

// persist the MessageChannel object so we can use it to communicate with the parent window
var iframeMessagePort = null;

/**
* DOM Event handlers to power the recovery and auth flows in standalone mode
* Instead of receiving events from the parent page, forms trigger them.
* This is useful for debugging as well.
*/
var addDOMEventListeners = function () {
document.getElementById("inject").addEventListener(
"click",
async function (e) {
e.preventDefault();
window.postMessage({
type: "INJECT_CREDENTIAL_BUNDLE",
value: document.getElementById("credential-bundle").value,
});
},
false
);
document.getElementById("stamp").addEventListener(
"click",
async function (e) {
e.preventDefault();
window.postMessage({
type: "STAMP_REQUEST",
value: document.getElementById("payload").value,
});
},
false
);
document.getElementById("reset").addEventListener(
"click",
async function (e) {
e.preventDefault();
window.postMessage({ type: "RESET_EMBEDDED_KEY" });
},
false
);
}

/**
* Message Event Handlers to process messages from the parent frame
*/
var messageEventListener = async function(event) {
if (
event.data &&
(event.data["type"] == "INJECT_CREDENTIAL_BUNDLE" ||
event.data["type"] == "INJECT_RECOVERY_BUNDLE")
) {
TKHQ.logMessage(
`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`
);
try {
await onInjectBundle(event.data["value"]);
} catch (e) {
TKHQ.sendMessageUp("ERROR", e.toString());
}
}
if (event.data && event.data["type"] == "STAMP_REQUEST") {
TKHQ.logMessage(
`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`
);
try {
await onStampRequest(event.data["value"]);
} catch (e) {
TKHQ.sendMessageUp("ERROR", e.toString());
}
}
if (event.data && event.data["type"] == "RESET_EMBEDDED_KEY") {
TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}`);
try {
TKHQ.onResetEmbeddedKey();
} catch (e) {
TKHQ.sendMessageUp("ERROR", e.toString());
}
}
}

/**
* Initialize the embedded key and set up the DOM and message event listeners
*/
document.addEventListener(
"DOMContentLoaded",
async function () {
Expand All @@ -1014,88 +1115,40 @@ <h2>Message log</h2>
var targetPubBuf = await TKHQ.p256JWKPrivateToPublic(embeddedKeyJwk);
var targetPubHex = TKHQ.uint8arrayToHexString(targetPubBuf);
document.getElementById("embedded-key").value = targetPubHex;
TKHQ.sendMessageUp("PUBLIC_KEY_READY", targetPubHex);

// TODO: find a way to filter messages and ensure they're coming from the parent window?
// We do not want to arbitrarily receive messages from all origins.

window.addEventListener(
"message",
async function (event) {
if (
event.data &&
(event.data["type"] == "INJECT_CREDENTIAL_BUNDLE" ||
event.data["type"] == "INJECT_RECOVERY_BUNDLE")
) {
TKHQ.logMessage(
`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`
);
try {
await onInjectBundle(event.data["value"]);
} catch (e) {
TKHQ.sendMessageUp("ERROR", e.toString());
}
}
if (event.data && event.data["type"] == "STAMP_REQUEST") {
TKHQ.logMessage(
`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}`
);
try {
await onStampRequest(event.data["value"]);
} catch (e) {
TKHQ.sendMessageUp("ERROR", e.toString());
}
}
if (event.data && event.data["type"] == "RESET_EMBEDDED_KEY") {
TKHQ.logMessage(`⬇️ Received message ${event.data["type"]}`);
try {
TKHQ.onResetEmbeddedKey();
} catch (e) {
TKHQ.sendMessageUp("ERROR", e.toString());
}
}
},
messageEventListener,
false
);

/**
* Event handlers to power the recovery and auth flows in standalone mode
* Instead of receiving events from the parent page, forms trigger them.
* This is useful for debugging as well.
*/
document.getElementById("inject").addEventListener(
"click",
async function (e) {
e.preventDefault();
window.postMessage({
type: "INJECT_CREDENTIAL_BUNDLE",
value: document.getElementById("credential-bundle").value,
});
},
false
);
document.getElementById("stamp").addEventListener(
"click",
async function (e) {
e.preventDefault();
window.postMessage({
type: "STAMP_REQUEST",
value: document.getElementById("payload").value,
});
},
false
);
document.getElementById("reset").addEventListener(
"click",
async function (e) {
e.preventDefault();
window.postMessage({ type: "RESET_EMBEDDED_KEY" });
},
false
);
addDOMEventListeners();

TKHQ.sendMessageUp("PUBLIC_KEY_READY", targetPubHex);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried that if this sendMessageUp function is called before TURNKEY_INIT_MESSAGE_CHANNEL is received (and thus, the channel set up), then the message will fail to be received on the other end (parent frame) because new SDKs won't listen for raw messages coming from postMessage anymore.

So it seems there's a bit of a race condition, and we should wait to receive "TURNKEY_INIT_MESSAGE_CHANNEL" before broadcasting "PUBLIC_KEY_READY"?

Another way to handle this is to broadcast PUBLIC_KEY_READY twice:

  • leave this here the way you have it (for old SDKs)
  • add a call to sendMessageUp("PUBLIC_KEY_READY", targetPubHex) again right after TKHQ.setParentFrameMessageChannelPort is called (so that new SDKs receive it as well)

Now that I'm writing this I think that's probably the cleanest option?

},
false
);

window.addEventListener("message", async function (event) {
/**
* @turnkey/iframe-stamper >= v2.1.0 is using a MessageChannel to communicate with the parent frame.
* The parent frame sends a TURNKEY_INIT_MESSAGE_CHANNEL event with the MessagePort.
* If we receive this event, we want to remove the message event listener that was added in the DOMContentLoaded event to avoid processing messages twice.
* We persist the MessagePort so we can use it to communicate with the parent window in subsequent calls to TKHQ.sendMessageUp
*/
if (event.data && event.data["type"] == "TURNKEY_INIT_MESSAGE_CHANNEL" && event.ports?.[0]) {
// remove the message event listener that was added in the DOMContentLoaded event
window.removeEventListener("message", messageEventListener, false);
iframeMessagePort = event.ports[0];
iframeMessagePort.onmessage = messageEventListener

TKHQ.setParentFrameMessageChannelPort(iframeMessagePort);

// gets the embedded key value that was created in the DOMContentLoaded handler and sends it to the parent frame
TKHQ.sendMessageUp("PUBLIC_KEY_READY", document.getElementById("embedded-key").value);
}
});

/**
* Function triggered when INJECT_CREDENTIAL_BUNDLE event is received.
* The `bundle` param is the concatenation of a public key and an encrypted payload, and then base64 encoded
Expand Down
Loading
Loading