Skip to content

Commit

Permalink
Merge pull request #56 from tkhq/eng-1441
Browse files Browse the repository at this point in the history
implement message channel
  • Loading branch information
turnekybc authored Dec 20, 2024
2 parents 633fbca + 48b0c34 commit d8a6e18
Show file tree
Hide file tree
Showing 3 changed files with 382 additions and 222 deletions.
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,
})
} 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);
},
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

0 comments on commit d8a6e18

Please sign in to comment.