Skip to content

Instantly share code, notes, and snippets.

@notkurt
Last active April 22, 2024 15:33
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save notkurt/d9a23f9f735653c45183eddf7b19280a to your computer and use it in GitHub Desktop.
Save notkurt/d9a23f9f735653c45183eddf7b19280a to your computer and use it in GitHub Desktop.
New Customer Accounts Login Shim
/*
Allows you to hijack the new Customer Accounts login flow and redirect to a custom page after login.
10SQ - 2024
10sq.dev
*/
const SHOP_ID = "<YOUR-SHOP-ID>";
const CLIENT_ID = "<YOUR-CLIENT-ID>"; // Install https://apps.shopify.com/headless
const CALLBACK_URL = "<YOUR-STORE-URL>"
/* Login Logic */
function generateNonce(length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let nonce = "";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
nonce += characters.charAt(randomIndex);
}
return nonce;
}
function base64UrlEncode(str) {
const base64 = btoa(str);
// This is to ensure that the encoding does not have +, /, or = characters in it.
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
function generateRandomCode() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return String.fromCharCode.apply(null, Array.from(array));
}
function generateState() {
const timestamp = Date.now().toString();
const randomString = Math.random().toString(36).substring(2);
return timestamp + randomString;
}
async function generateCodeVerifier() {
const rando = generateRandomCode();
return base64UrlEncode(rando);
}
async function generateCodeChallenge(codeVerifier) {
const digestOp = await crypto.subtle.digest(
{ name: "SHA-256" },
new TextEncoder().encode(codeVerifier)
);
const hash = convertBufferToString(digestOp);
return base64UrlEncode(hash);
}
function convertBufferToString(hash) {
const uintArray = new Uint8Array(hash);
const numberArray = Array.from(uintArray);
return String.fromCharCode(...numberArray);
}
async function redirectToShopifyAuth(shopId, redirectUri, state, nonce) {
const clientId = CLIENT_ID;
const authorizationRequestUrl = new URL(
`https://shopify.com/${shopId}/auth/oauth/authorize`
);
authorizationRequestUrl.searchParams.append(
"scope",
"openid email https://api.customers.com/auth/customer.graphql"
);
authorizationRequestUrl.searchParams.append("client_id", clientId);
authorizationRequestUrl.searchParams.append("response_type", "code");
authorizationRequestUrl.searchParams.append("redirect_uri", redirectUri);
authorizationRequestUrl.searchParams.append("state", state);
authorizationRequestUrl.searchParams.append("nonce", nonce);
const verifier = await generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
localStorage.setItem("code-verifier", verifier);
authorizationRequestUrl.searchParams.append("code_challenge", challenge);
authorizationRequestUrl.searchParams.append("code_challenge_method", "S256");
window.location.href = authorizationRequestUrl.toString();
}
/* Hijack Logic */
const triggerHijack = (event) => {
window.console.log("Login shim ready...");
const nonce = generateNonce(16);
const state = generateState();
// write current path to local storage
localStorage.setItem("hijack-path", window.location.pathname);
redirectToShopifyAuth(
SHOP_ID,
CALLBACK_URL,
state,
nonce
);
};
const loginButtons = document.querySelectorAll("[data-account-hijack]");
loginButtons.forEach((button) => {
button.addEventListener("click", (event) => {
event.preventDefault();
triggerHijack();
});
});
const showLoginPanel = () => {
const el = document.getElementById("login-loader");
if (el) {
el.classList.add("active");
}
};
const hideLoginPanel = () => {
const el = document.getElementById("login-loader");
if (el) {
el.classList.remove("active");
}
};
const triggerSsoEvent = () => {
fetch("/customer_identity/sso_hint?accountnumber=0")
.then((response) => {
// Handle the response here if the request was successful.
console.log("Request succeeded:", response);
})
.catch((error) => {
// Handle any errors here if the request failed.
console.log("Request failed:", error);
})
.finally(() => {
// Need to look into this, but it will claim to work even if the request failed. Something to do with a redirect chain.
console.log("Request completed. Performing follow-up action...");
// get hijack-path from local storage
const path = localStorage.getItem("hijack-path");
if (path != undefined && path != "") {
// clear hijack-path from local storage
localStorage.removeItem("hijack-path");
history.replaceState(null, '', path);
window.location.replace(path);
}
});
};
/* if code= and state= in query params, then hijack */
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("code") && urlParams.has("state")) {
window.console.log("Ready to redirect login shim...");
showLoginPanel();
triggerSsoEvent();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment