Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Oauth #19

Merged
merged 6 commits into from

4 participants

@redronin
Owner

@jduff @davefp

Update to get api gem working with Oauth2.

Dave, this needs some documentation updates - a lot of it out of date. Also, probably need a warning saying it's only for Oauth2.

@redronin
Owner

@jduff @davefp
Updated with using header, fixed ActiveResource::Base issue, added back validates_signature.

@jduff
Owner

Looks good. Only question is if we should replace all those comments with something updated. Can be done later on though.

When we merge this in we want to make sure to do a major version bump.

@davefp
@davefp davefp merged commit cf23a8f into Shopify:master
@gadogado

Hey guys heads up - #create_permission_url is still referenced within your README.rdoc under the Getting Started section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 28, 2012
  1. @redronin

    work with oauth2

    redronin authored
Commits on Mar 29, 2012
  1. @redronin
Commits on Mar 30, 2012
  1. @redronin
  2. @redronin
  3. @redronin

    Update tests

    redronin authored
    Add tests
  4. @redronin

    Add back validates signature

    redronin authored
This page is out of date. Refresh to see the latest.
View
22 lib/shopify_api/resources/base.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
121 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
@@ -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
50 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
View
29 test/session_test.rb
@@ -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
Something went wrong with that request. Please try again.