Last active
July 24, 2024 19:20
-
-
Save notkurt/d9a23f9f735653c45183eddf7b19280a to your computer and use it in GitHub Desktop.
New Customer Accounts Login Shim
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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