Permalink
Browse files

Merge pull request #19 from redronin/oauth

replaces the legacy auth code with Oauth2 functionality
  • Loading branch information...
2 parents fd6cabf + bc4a797 commit cf23a8fa1d1fbdfef3f98e2116c8624a9938043a @davefp davefp committed Apr 5, 2012
Showing with 104 additions and 118 deletions.
  1. +22 −0 lib/shopify_api/resources/base.rb
  2. +15 −106 lib/shopify_api/session.rb
  3. +50 −0 test/base_test.rb
  4. +17 −12 test/session_test.rb
@@ -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 => [])
View
@@ -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
@@ -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
@@ -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
View
@@ -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
View
@@ -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'})
@@ -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.