CORS and Shopify

the blog image

Oct 27, 2013

In the past there have been more than a few developers totally lost when it comes to dealing with Shopify, Ajax, Apps and cross-domain coding issues. A little history can perhaps explain this. Javascript in the browser on desktop computers is useful as a scripting tool and can be leveraged for nefarious purposes. On a mobile phone it is less of a problem since every App on a phone has it’s own protective little cubby hole. The JS evolution came up with JSONP to handle cross-domain responses, and that works well if you’re not Resty and you like GET requests all the time for everything. Read into that “it’s a wee bit awkward”.

Jumping forward, some brainiacs came up with a better system called CORS or Cross-Origin Resource Sharing. Nice acronym. I want to get at some resources on a different origin, so this is the way to go.

Some developers have skimmed the Shopify API and seeing a bunch of JSON and some basic POST requests they get naked and jump into the party head-first thinking they are going to make a super cool Shopify hack that will wow their benefactors. They forget that the Shopify API is protected with authentication tokens that should never be present in a client-side code dump like Shop rendering. They can use the Shopify Ajax code to get a cart, add/remove items from a cart, format money, and many other front-end tasks, but there is not a whole lot going on that is risky there. A script kiddie can GET a resource and pick apart some internals, but not much else. For instance some Apps pass themselves off as “price” manipulation masters but anyone can simply view all the variants and their prices with no trouble using this front-end Shopify Ajax code.

Getting information to and from the back-end of a Shop is the goal here, and we need to do it with some reasonable security. One sure way to be secure is to float an App in the cloud that is installed in a shop. Consider an App like that to be nothing more than a Proxy. It can authenticate to the shop and using the Shopify API it can create, read, update or delete resources (CRUD). Since floating an App typically means running a web server of some sort, we have the ability to now build in CORS protection. That means we can decide many things about incoming requests that make us happy or sad.

Something that makes us happy is a crude check that the request is providing the HTTP_ORIGIN of where it is coming from. That can be spoofed of course, but we definitely do not want to answer the bell and say we’re home and open for business if it is not part of the request. Second, we can be happy if the origin is in fact our Shopify site. So if we check that fizz buzz.myshopify.com is the origin of our request we are happy and we continue allowing the request to be processed.

Now we want to restrict the incoming request to be one of GET, POST, PUT or DELETE. CORS comes with coverage for those. So we can ensure that the request coming in is a POST if we want. We process it and send back JSON to the original XHR sender. As a quick example the request payload could be a logged in customer’s email address and we want to return their tags, or we want to add a new tag to a customer. Or we want to do some other work that the App is approved to do. So now the developer can in fact write some Javascript code to affect a customer’s session in a shop. The callbacks will work, and there are no cross-domain origin refusals such as many developers experience with Shopify when they get mixed up about how to handle this.

A basic simple demo here uses a neat little gem called sinatra-cross_origin that was written to wrap or encapsulate the mundane CORS settings into a helper method.

config.ru gets our Basic Rack App up and running, authentication to a shop with permissions to read/write customers via the API

require 'bundler/setup'
Bundler.require
 
require './app'

SCOPE = 'write_customers'

use OmniAuth::Builder do
  provider :shopify, ENV['SHOPIFY_API_KEY'], ENV['SHOPIFY_API_SECRET'],
           :scope => SCOPE,
           :setup => lambda { |env| params = Rack::Utils.parse_query(env['QUERY_STRING'])
             env['omniauth.strategy'].options[:client_options][:site] = "http://#{params['shop']}" }
end

run App::Shopify

# app.rb is where the Sinatra web server deals with CORS POST request to /customer/email/validate

module App
  class Shopify < Sinatra::Base
    
    register Sinatra::CrossOrigin

    post '/customer/email' do
      halt 403 unless request.env['HTTP_ORIGIN']
      origin = request.env['HTTP_ORIGIN']
      # the shop is known as fizzbuzz.myshopify.com
      if origin =~ /fizzbuzz/                        
        cross_origin :allow_origin => origin 
        puts "Customer email might be: #{params[:email]}"
        # todo: some neat stuff with the incoming parameters and the Shopify API
        content_type :json
        {:success => true}.to_json
      else
        halt 403, "Illegal CORS call from #{origin}"
      end
    end
    
  end
end