Skip to content

Instantly share code, notes, and snippets.

@paul-phan
Last active October 7, 2021 09:56
Show Gist options
  • Select an option

  • Save paul-phan/db244b9fc3ce67c247e9f46991693c1d to your computer and use it in GitHub Desktop.

Select an option

Save paul-phan/db244b9fc3ce67c247e9f46991693c1d to your computer and use it in GitHub Desktop.
Custom Shopify Auth
export function safeCompare(
strA: string | Record<string, string> | string[] | number[],
strB: string | Record<string, string> | string[] | number[]
): boolean {
if (typeof strA === typeof strB) {
let buffA: Buffer
let buffB: Buffer
if (typeof strA === 'object' && typeof strB === 'object') {
buffA = Buffer.from(JSON.stringify(strA))
buffB = Buffer.from(JSON.stringify(strB))
} else {
// @ts-ignore
buffA = Buffer.from(strA)
// @ts-ignore
buffB = Buffer.from(strB)
}
if (buffA.length === buffB.length) {
return crypto.timingSafeEqual(buffA, buffB)
}
}
return false
}
export function stringifyQuery(query: AuthQuery): string {
const orderedObj = Object.keys(query)
.sort((val1, val2) => val1.localeCompare(val2))
.reduce((obj: Record<string, string | undefined>, key: keyof AuthQuery) => {
obj[key] = query[key]
return obj
}, {})
return querystring.stringify(orderedObj)
}
export function generateLocalHmac({code, timestamp, state, shop, host}: AuthQuery): string {
const queryString = stringifyQuery({
code,
timestamp,
state,
shop,
...host && {host}
})
return crypto
.createHmac('sha256', Shopify.Context.API_SECRET_KEY)
.update(queryString)
.digest('hex')
}
export function validateHmac(query): boolean {
if (!query.hmac) {
return false
}
const {hmac} = query
const localHmac = generateLocalHmac(query)
return safeCompare(hmac as string, localHmac)
}
export async function customHandleAuthCallback(request) {
const {query} = request
const {code, shop} = query
if (shop == null) {
console.error('Expected a shop query parameter')
return null
}
if (!validateHmac(query)) {
console.error('Failed to validate HMAC!', query, Shopify.Context.API_SECRET_KEY)
return null
}
try {
const requestBody = querystring.stringify({
code,
client_id: Shopify.Context.API_KEY,
client_secret: Shopify.Context.API_SECRET_KEY
})
let responseBody: any = await got.post(`https://${shop}/admin/oauth/access_token`, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
// @ts-ignore
'Content-Length': Buffer.byteLength(requestBody)
},
body: requestBody
}).text()
if (typeof responseBody === 'string') {
responseBody = JSON.parse(responseBody)
}
// @ts-ignore
const accessToken = responseBody.access_token
if (accessToken) {
return {accessToken, shop}
} else {
console.warn('Access token not generated.')
return null
}
} catch (error) {
console.error('🔴 Error storing shop access token', error)
return null
}
}
/* Usage in authCallBack */
function handleShopifyAuthCallback = async (req, res) => {
try {
try {
await Shopify.Auth.validateAuthCallback(req, res, req.query as unknown as AuthQuery) // req.query must be cast to unknown and then AuthQuery in order to be accepted
} catch (e) {
console.warn('validateAuthCallback failed!', e)
}
let currentSession: any = await Shopify.Utils.loadCurrentSession(req, res)
if (!currentSession?.accessToken) {
currentSession = {...currentSession, ...await customHandleAuthCallback(req)}
}
if (currentSession.accessToken) {
let {shop, accessToken, scope} = currentSession
if (!scope) {
scope = shopifyApiScope.split(',')
currentSession.scope = scope
}
// Save session into express-session to get the shop info in the req.session object
req.session.shop = shop
req.session.accessToken = accessToken
let save: any = {
accessToken, scope
}
req.session.scope = scope
await Shopify.Context.SESSION_STORAGE.storeSession(currentSession)
return // handle after auth
} else {
return res.send('Failed to authenticate with Shopify!')
}
} catch (error) {
console.error(error) // in practice these should be handled more gracefully
return res.redirect('/')
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment