Skip to content

Commit

Permalink
Merge pull request #19 from redronin/oauth
Browse files Browse the repository at this point in the history
replaces the legacy auth code with Oauth2 functionality
  • Loading branch information
davefp committed Apr 5, 2012
2 parents fd6cabf + bc4a797 commit cf23a8f
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 118 deletions.
22 changes: 22 additions & 0 deletions lib/shopify_api/resources/base.rb
Expand Up @@ -8,6 +8,28 @@ class Base < ActiveResource::Base
"ActiveResource/#{ActiveResource::VERSION::STRING}",
"Ruby/#{RUBY_VERSION}"].join(' ')

class << self
def headers
if defined?(@headers)
@headers
elsif superclass != Object && superclass.headers
superclass.headers
else
@headers ||= {}
end
end

def activate_session(session)
self.site = session.site
self.headers.merge!('X-Shopify-Access-Token' => session.token)
end

def clear_session
self.site = nil
self.headers.delete('X-Shopify-Access-Token')
end
end

private
def only_id
encode(:only => :id, :include => [], :methods => [])
Expand Down
121 changes: 15 additions & 106 deletions lib/shopify_api/session.rb
@@ -1,84 +1,6 @@

module ShopifyAPI
#
# The Shopify API authenticates each call via HTTP Authentication, using
# * the application's API key as the username, and
# * a hex digest of the application's shared secret and an
# authentication token as the password.
#
# Generation & acquisition of the beforementioned looks like this:
#
# 0. Developer (that's you) registers Application (and provides a
# callback url) and receives an API key and a shared secret
#
# 1. User visits Application and are told they need to authenticate the
# application first for read/write permission to their data (needs to
# happen only once). User is asked for their shop url.
#
# 2. Application redirects to Shopify : GET <user's shop url>/admin/api/auth?api_key=<API key>
# (See Session#create_permission_url)
#
# 3. User logs-in to Shopify, approves application permission request
#
# 4. Shopify redirects to the Application's callback url (provided during
# registration), including the shop's name, and an authentication token in the parameters:
# GET client.com/customers?shop=snake-oil.myshopify.com&t=a94a110d86d2452eb3e2af4cfb8a3828
#
# 5. Authentication password computed using the shared secret and the
# authentication token (see Session#computed_password)
#
# 6. Profit!
# (API calls can now authenticate through HTTP using the API key, and
# computed password)
#
# LoginController and ShopifyLoginProtection use the Session class to set Shopify::Base.site
# so that all API calls are authorized transparently and end up just looking like this:
#
# # get 3 products
# @products = ShopifyAPI::Product.find(:all, :params => {:limit => 3})
#
# # get latest 3 orders
# @orders = ShopifyAPI::Order.find(:all, :params => {:limit => 3, :order => "created_at DESC" })
#
# As an example of what your LoginController should look like, take a look
# at the following:
#
# class LoginController < ApplicationController
# def index
# # Ask user for their #{shop}.myshopify.com address
# end
#
# def authenticate
# redirect_to ShopifyAPI::Session.new(params[:shop]).create_permission_url
# end
#
# # Shopify redirects the logged-in user back to this action along with
# # the authorization token t.
# #
# # This token is later combined with the developer's shared secret to form
# # the password used to call API methods.
# def finalize
# shopify_session = ShopifyAPI::Session.new(params[:shop], params[:t])
# if shopify_session.valid?
# session[:shopify] = shopify_session
# flash[:notice] = "Logged in to shopify store."
#
# return_address = session[:return_to] || '/home'
# session[:return_to] = nil
# redirect_to return_address
# else
# flash[:error] = "Could not log in to Shopify store."
# redirect_to :action => 'index'
# end
# end
#
# def logout
# session[:shopify] = nil
# flash[:notice] = "Successfully logged out."
#
# redirect_to :action => 'index'
# end
# end
#

class Session
cattr_accessor :api_key
cattr_accessor :secret
Expand All @@ -95,13 +17,18 @@ def setup(params)

def temp(domain, token, &block)
session = new(domain, token)
begin
original_domain = URI.parse(ShopifyAPI::Base.site.to_s).host
rescue URI::InvalidURIError
end
original_token = ShopifyAPI::Base.headers['X-Shopify-Access-Token']
original_session = new(original_domain, original_token)

original_site = ShopifyAPI::Base.site
begin
ShopifyAPI::Base.site = session.site
ShopifyAPI::Base.activate_session(session)
yield
ensure
ShopifyAPI::Base.site = original_site
ShopifyAPI::Base.activate_session(original_session)
end
end

Expand All @@ -110,56 +37,38 @@ def prepare_url(url)
url.gsub!(/https?:\/\//, '') # remove http:// or https://
url.concat(".myshopify.com") unless url.include?('.') # extend url to myshopify.com if no host is given
end

def validate_signature(params)
return false unless signature = params[:signature]

sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
Digest::MD5.hexdigest(secret + sorted_params) == signature
end

end

def initialize(url, token = nil, params = nil)
self.url, self.token = url, token
self.class.prepare_url(self.url)

if params
unless self.class.validate_signature(params) && params[:timestamp].to_i > 24.hours.ago.utc.to_i
raise "Invalid Signature: Possible malicious login"
end
end

self.class.prepare_url(self.url)
end

def shop
Shop.current
end

def create_permission_url
return nil if url.blank? || api_key.blank?
"http://#{url}/admin/api/auth?api_key=#{api_key}"
end

# Used by ActiveResource::Base to make all non-authentication API calls
#
# (ShopifyAPI::Base.site set in ShopifyLoginProtection#shopify_session)
def site
"#{protocol}://#{api_key}:#{computed_password}@#{url}/admin"
"#{protocol}://#{url}/admin"
end

def valid?
url.present? && token.present?
end

private

# The secret is computed by taking the shared_secret which we got when
# registring this third party application and concating the request_to it,
# and then calculating a MD5 hexdigest.
def computed_password
Digest::MD5.hexdigest(secret + token.to_s)
end


end
end
50 changes: 50 additions & 0 deletions test/base_test.rb
@@ -0,0 +1,50 @@
require 'test_helper'


class BaseTest < Test::Unit::TestCase

def setup
@session1 = ShopifyAPI::Session.new('shop1.myshopify.com', 'token1')
@session2 = ShopifyAPI::Session.new('shop2.myshopify.com', 'token2')
end

test '#activate_session should set site and headers for given session' do
ShopifyAPI::Base.activate_session @session1

assert_nil ActiveResource::Base.site
assert_equal 'https://shop1.myshopify.com/admin', ShopifyAPI::Base.site.to_s
assert_equal 'https://shop1.myshopify.com/admin', ShopifyAPI::Shop.site.to_s

assert_nil ActiveResource::Base.headers['X-Shopify-Access-Token']
assert_equal 'token1', ShopifyAPI::Base.headers['X-Shopify-Access-Token']
assert_equal 'token1', ShopifyAPI::Shop.headers['X-Shopify-Access-Token']
end

test '#clear_session should clear site and headers from Base' do
ShopifyAPI::Base.activate_session @session1
ShopifyAPI::Base.clear_session

assert_nil ActiveResource::Base.site
assert_nil ShopifyAPI::Base.site
assert_nil ShopifyAPI::Shop.site

assert_nil ActiveResource::Base.headers['X-Shopify-Access-Token']
assert_nil ShopifyAPI::Base.headers['X-Shopify-Access-Token']
assert_nil ShopifyAPI::Shop.headers['X-Shopify-Access-Token']
end

test '#activate_session with one session, then clearing and activating with another session should send request to correct shop' do
ShopifyAPI::Base.activate_session @session1
ShopifyAPI::Base.clear_session
ShopifyAPI::Base.activate_session @session2

assert_nil ActiveResource::Base.site
assert_equal 'https://shop2.myshopify.com/admin', ShopifyAPI::Base.site.to_s
assert_equal 'https://shop2.myshopify.com/admin', ShopifyAPI::Shop.site.to_s

assert_nil ActiveResource::Base.headers['X-Shopify-Access-Token']
assert_equal 'token2', ShopifyAPI::Base.headers['X-Shopify-Access-Token']
assert_equal 'token2', ShopifyAPI::Shop.headers['X-Shopify-Access-Token']
end

end
29 changes: 17 additions & 12 deletions test/session_test.rb
Expand Up @@ -23,7 +23,7 @@ class SessionTest < Test::Unit::TestCase
session = ShopifyAPI::Session.new("testshop.myshopify.com", "any-token")
end
end

should "raise error if params passed but signature omitted" do
assert_raises(RuntimeError) do
session = ShopifyAPI::Session.new("testshop.myshopify.com", "any-token", {'foo' => 'bar'})
Expand All @@ -41,25 +41,30 @@ class SessionTest < Test::Unit::TestCase
end

should "#temp reset ShopifyAPI::Base.site to original value" do
ShopifyAPI::Base.site = 'http://www.original.com'


ShopifyAPI::Session.setup(:api_key => "key", :secret => "secret")
assigned_site = nil
session1 = ShopifyAPI::Session.new('fakeshop.myshopify.com', 'token1')
ShopifyAPI::Base.activate_session(session1)

ShopifyAPI::Session.temp("testshop.myshopify.com", "any-token") {
assigned_site = ShopifyAPI::Base.site
@assigned_site = ShopifyAPI::Base.site
}
assert_equal 'https://key:e56d5793b869753d87cf03ceb6bb5dfc@testshop.myshopify.com/admin', assigned_site.to_s
assert_equal 'http://www.original.com', ShopifyAPI::Base.site.to_s
assert_equal 'https://testshop.myshopify.com/admin', @assigned_site.to_s
assert_equal 'https://fakeshop.myshopify.com/admin', ShopifyAPI::Base.site.to_s
end

should "return permissions url" do
should "return site for session" do
session = ShopifyAPI::Session.new("testshop.myshopify.com", "any-token")
assert_equal "http://testshop.myshopify.com/admin/api/auth?api_key=key", session.create_permission_url
assert_equal "https://testshop.myshopify.com/admin", session.site
end

should "return site for session" do
session = ShopifyAPI::Session.new("testshop.myshopify.com", "any-token")
assert_equal "https://key:e56d5793b869753d87cf03ceb6bb5dfc@testshop.myshopify.com/admin", session.site
should "raise error if signature does not match expected" do
ShopifyAPI::Session.secret = 'secret'
params = {:foo => 'hello', :foo => 'world', :timestamp => Time.now}
sorted_params = params.except(:signature, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join
signature = Digest::MD5.hexdigest(ShopifyAPI::Session.secret + sorted_params)

session = ShopifyAPI::Session.new("testshop.myshopify.com", "any-token", params.merge(:signature => signature))
end
end
end

0 comments on commit cf23a8f

Please sign in to comment.