Last active
October 7, 2021 09:56
-
-
Save paul-phan/db244b9fc3ce67c247e9f46991693c1d to your computer and use it in GitHub Desktop.
Custom Shopify Auth
This file contains hidden or 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
| 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