Today we’ll be going through the Shopify OAuth process using Express JS. Before we begin make sure you have the following:

Step 0: Firing up ngrok

We can’t do anything from localhost, so fire up a HTTP tunnel using Ngrok. Make sure you run the command below from the directory containing the Ngrok executable.

./ngrok http 3000

Our project uses Dotenv, which is a simple module that loads environment variables from a .env file into process.env. Our .env file is in the root of our project. Open it and create a new entry called ROOT_URL thats set to our https Ngrok url.

PORT=3000
DB=mongodb://localhost/shopify-oauth
ROOT_URL=https://82a99d60.ngrok.io

Leave ngrok running the entire length of this tutorial. As a basic user every time you restart ngrok you’ll be given a new URL and will have to reset this value a bunch of places.

Step 1: Get the client’s credentials

Head over to your Shopify Partners Dashboard and create a new application (Apps > Create App).

It will ask you for an app name and a URL. Name it whatever, but use the Ngrok forwarded HTTPS URL. Creation will take you to your apps settings page. Click the App info tab at the top, then scroll down until you find your app’s credentials.

Open your .env file again and add two new entries for these values.

PORT=3000
DB=mongodb://localhost/shopify-oauth
ROOT_URL=https://82a99d60.ngrok.io
SHOPIFY_API_KEY=d54987cd7ef66434f3590bee4aad22e8
SHOPIFY_API_SECRET=164b88a261723224e396e66e2c1f096f

We can start our app at anytime by running npm run start. Anytime we change our .env file we will have to manually restart our app.

Step 2: Ask for permission

When someone clicks install from the Shopify App Store it will send them to your application authentication page with a shop query parameter attached. Our job is to redirect the user using this shop parameter. Shopify gives a template for our redirect:

https://{shop}/admin/oauth/authorize?client_id={api_key}&scope={scopes}&redirect_uri={redirect_uri}&state={nonce}

Let’s breakdown all the parameters required to craft this url:

  • Shop - The shop url (shopname.myshopify.com) that’s installing our app.
  • Client ID - This is our app’s API key which is stored in our .env file.
  • Scopes - This is a list of permissions that we’re requesting. You can see the available scopes here.
  • Redirect URI - This is the URL that we want shopify to send the user to after they click install.
  • Nonce - This is a random key that we will provide.

Let’s create a helper function to craft these redirect URLs for us. Open the file server/helpers/redirect.js. We’ll assume we’re passing in shop and nonce as parameters.

module.exports = (shop, nonce) => {
    // Grab our API key
    const client_id = process.env.SHOPIFY_API_KEY

    // Example scopes
    const scopes = 'read_script_tags,write_script_tags'

    // Our callback URI
    const redirect_uri = process.env.ROOT_URL + '/auth/callback'

    // Put it all together and return
    return `https://${shop}/admin/oauth/authorize?client_id=${client_id}&scope=${scopes}&redirect_uri=${redirect_uri}&state=${nonce}`
}

Open the file server/controllers/auth.js. Let’s instruct express how to handle requests to /auth. At the end we’ll redirect the user.

exports.auth = (req, res) => {
    // Extract shop
    const { shop } = req.query

    // Send error if no shop provided in url parameters
    const error = 'Missing shop name!'
    if (!shop) return res.status(422).send({error})

    // Create Nonce (random string)
    const nonce = crypto.randomBytes(20).toString('hex')

    // Store nonce in session cookie
    req.session.nonce = nonce

    // Craft redirect
    const url = redirect(shop, nonce)

    // Redirect User
    res.redirect(url)
}

Note: We store the nonce in a session cookie because we have to validate that it stayed consistent in the next step. This is required to keep your app secure.

Lets try it out! Head to your auth URL using the Ngrok https URL. Remember to attach your development store url as a parameter. It should look similar to mine:

https://82a99d60.ngrok.io/auth?shop=rkteststore.myshopify.com

Note: If your browser complains about the SSL certificate click advanced and add an exception/bypass.

If all went well you should see a beautiful error message Oauth error invalid_request: The redirect_uri is not whitelisted.

No problem, we just have to whitelist the callback url in our app’s setting page (in your Shopify partners dashboard). Head to the App info tab of your apps settings page, and add the callback url under Whitelisted redirection URL(s). It should look similar to mine:

Save your changes and try the auth URL again. This time It should take you to an install confirmation page:

Click install and it should redirect to our callback page with a bunch of query parameters.

https://82a99d60.ngrok.io/auth/callback?code=4d856955864c46542f26fd5a5e45115c&hmac=fe5fecd7a15398b21d2e94ae4a3c9692147fe39d7ba6e0858fd41eeddb32f91d&shop=rkteststore.myshopify.com&state=c93e45805f884dc1592895b7e44fb885b87f1113&timestamp=1515964766

Step 3: Confirm installation

Before we can go any further we have to run a bunch of checks to validate that this callback indeed came from Shopify. There’s 3 verifications we must make.

  • Ensure the nonce (state) is the same one that we provided.
  • Ensure the shop url is valid
  • Ensure the Hmac is valid

We’ll make these checks inside our callback function inside our authentication controller.

Ensuring the nonce is the same one we provided

This part’s easy. We can retrieve it from our session cookie and compare it with the parameter state.

const ourNonce = req.session.nonce
const theirNonce = req.query.state

if (ourNonce !== theirNonce) {
    return res.status(422).send({
        error: 'Nonce invalid'
    }) 
}

Ensuring the shop name is legitimate

This part’s easy aswell. We just have to confirm that the shop parameter meets the following:

  • ends with myshopify.com
  • does not contain characters other than letters (a-z), numbers (0-9), dots, and hyphens.

A simple regex can text this. The check looks like this:

const { shop } = req.query
const re = new RegExp('[a-zA-z0-9_\\-\\.]+(.myshopify.com$)')

if (!re.test(shop))  {
    return res.status(422).send({
        error: 'Invalid shop'
    }) 
}

Ensuring the hmac is legitimate

This part is a little more difficult. The hmac is an encrypted string that was made using our SHOPIFY_SECRET_KEY. We have to create our own version of the hmac the same way that Shopify did then verify the two are equal.

Here’s how Shopify instructs us to create it:

  1. We must convert the query parameters to a map.
  2. We must remove the hmac key from the map.
  3. The characters & and % are replaced with %26 and %25 respectively in keys and values. Additionally the = character is replaced with %3D in keys.
  4. Each key is concatenated with its value, separated by an = character, to create a list of strings. The list of key-value pairs is sorted lexicographically, and concatenated together with & to create a single string.
  5. Lastly, this string is processed through an HMAC-SHA256 using the Secret key as the key.

We’ll create another helper function. Open the server/helpers/hmac. We’ll pass req.query in as a parameter. Here’s the function.

module.exports = query => {
    // Create a map from the query
    const map = JSON.parse(JSON.stringify(query))

    // Delete the hmac key
    delete map['hmac']

    // Create a string from the map
    const string = qs.stringify(map, '&', '=', {
        encodeURIComponent: uri => {
            // Only encoding specific characters
            const replaced = uri
                .replace('&', '%26')
                .replace('%', '%25')
                .replace('=', '%3D')

            return replaced
        }
    })

    // Create hmac
    const generated_hmac = crypto
        // Using sha256 and our secret key
        .createHmac('sha256', process.env.SHOPIFY_API_SECRET)
        // with our string
        .update(string)
        // Using hex digest
        .digest('hex')

    // Return the hmac
    return generated_hmac
}

Now we can flip back to our auth controller and compare the hmacs.

const { query } = req
const theirHmac = query.hmac
const ourHmac = genHmac(query)

if (ourHmac !== theirHmac) {
    res.status(422).send({
        error: 'Hmac invalid!'
    })
}

The entire function should look like this so far.

exports.callback = (req, res) => {
    const ourNonce = req.session.nonce
    const theirNonce = req.query.state
    const { query } = req
    const { shop } = query
    const re = new RegExp('[a-zA-z0-9_\\-\\.]+(.myshopify.com$)')
    const theirHmac = query.hmac
    const ourHmac = genHmac(query)

    if (ourNonce !== theirNonce) {
        return res.status(422).send({
            error: 'Nonce invalid'
        }) 
    }

    if (!re.test(shop))  {
        return res.status(422).send({
            error: 'Invalid shop'
        }) 
    }

    if (ourHmac !== theirHmac) {
        return res.status(422).send({
            error: 'Hmac invalid!'
        })
    }

    res.send('All checks passed!')
}

Go to your development store admin panel and uninstall our app. Now test the whole process starting from our /auth route.

https://82a99d60.ngrok.io/auth?shop=rkteststore.myshopify.com

In everything’s working you should see All checks passed!.

Step 4: Getting an access token

So now that we’ve met all the Shopify security requirements, the authorization code can be exchanged once for a permanent access token. The exchange is made with a request to the shop.

https://{shop}.myshopify.com/admin/oauth/access_token

In the request body we must include:

  • client_id
  • client_secret
  • code (provided in the callback url query parameters)

We’ll make this request using the Request module in our authentication controller callback function (below our security checks).

exports.callback = (req, res) => {
    // ... 

    // Request for access token
    const url = `https://${shop}/admin/oauth/access_token`
    const client_id = process.env.SHOPIFY_API_KEY
    const client_secret = process.env.SHOPIFY_API_SECRET
    const body = { client_id, client_secret, code: query.code }
    const headers = { 'Content-Type': 'application/json' }

    request.post({ url, headers, body, json: true }, (err, resp, body) => {
        if (err) {
            return res.status(500).send({
                error: 'Something went wrong.'
            })
        }

        const { access_token } = body

        res.send(access_token)
    })
}

Go to your development store admin panel and uninstall our app. Now test the whole process starting from our /auth route.

https://82a99d60.ngrok.io/auth?shop=rkteststore.myshopify.com

This time if everything went well you should be presented with an access token. This token allows us to use any of the requested scope admin API’s on our user’s store.

Step 5: Saving our user

So at this point we have everything we need to save a user to our database. The user can install our app, we’re preforming all security checks, and we’re able to successfully receive an access token.

Inside our request function lets save a new user.

exports.callback = (req, res) => {
    // ... 

    request.post({ url, headers, body, json: true }, (err, resp, body) => {
        if (err) {
            return res.status(500).send({
                error: 'Something went wrong.'
            })
        }

        const { access_token } = body

        new User({ shop, access_token }).save().then(user => {
            res.render('index')
        })
    })
}

Go to your development store admin panel and uninstall our app. Now test the whole process starting from our /auth route.

https://82a99d60.ngrok.io/auth?shop=rkteststore.myshopify.com

This time it should create a user in our database and then send you to our application index page!

Step 6: The Index Route

So our application is installed and working. Everytime the user opens our app it will redirect them to the root path (/). They’ll also pass some query parameters including an hmac we need to verify. The url looks something like this:

https://82a99d60.ngrok.io/?hmac=6bb36ac4dd9b66202cfb02f7751f773d621ad56e665801921cb09eae9717e787&locale=en&protocol=https%3A%2F%2F&shop=rkteststore.myshopify.com&timestamp=1515971385"

We have to verify the Hmac is legitimate just like before, proving they are who they say they are.

exports.index = (req, res) => {
    const { query } = req
    const { shop } = query.shop 
    const theirHmac = query.hmac
    const ourHmac = genHmac(query)

    if (ourHmac !== theirHmac) {
        return res.status(422).send({
            error: 'Hmac invalid!'
        })
    }

    // If we make it to here we know it's a legitimate
    // request and they are req.query.shop

    res.render('index')
}

What’s next

That’s it for the oauth side, you still have to mix in your own form of authentication to verify subsequent requests. Have fun.