session.rb 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. # frozen_string_literal: true
  2. require 'openssl'
  3. require 'rack'
  4. module ShopifyAPI
  5. class ValidationException < StandardError
  6. end
  7. class Session
  8. SECONDS_IN_A_DAY = 24 * 60 * 60
  9. cattr_accessor :api_key, :secret, :myshopify_domain
  10. self.myshopify_domain = 'myshopify.com'
  11. attr_accessor :domain, :token, :name, :extra
  12. attr_reader :api_version, :access_scopes
  13. alias_method :url, :domain
  14. class << self
  15. def setup(params)
  16. params.each { |k, value| public_send("#{k}=", value) }
  17. end
  18. def temp(domain:, token:, api_version: ShopifyAPI::Base.api_version, &block)
  19. session = new(domain: domain, token: token, api_version: api_version)
  20. with_session(session, &block)
  21. end
  22. def with_session(session, &_block)
  23. original_session = extract_current_session
  24. original_user = ShopifyAPI::Base.user
  25. original_password = ShopifyAPI::Base.password
  26. begin
  27. ShopifyAPI::Base.clear_session
  28. ShopifyAPI::Base.activate_session(session)
  29. yield
  30. ensure
  31. ShopifyAPI::Base.activate_session(original_session)
  32. ShopifyAPI::Base.user = original_user
  33. ShopifyAPI::Base.password = original_password
  34. end
  35. end
  36. def with_version(api_version, &block)
  37. original_session = extract_current_session
  38. session = new(domain: original_session.site, token: original_session.token, api_version: api_version)
  39. with_session(session, &block)
  40. end
  41. def prepare_domain(domain)
  42. return nil if domain.blank?
  43. # remove http:// or https://
  44. domain = domain.strip.gsub(%r{\Ahttps?://}, '')
  45. # extract host, removing any username, password or path
  46. shop = URI.parse("https://#{domain}").host
  47. # extract subdomain of .myshopify.com
  48. if (idx = shop.index("."))
  49. shop = shop.slice(0, idx)
  50. end
  51. return nil if shop.empty?
  52. "#{shop}.#{myshopify_domain}"
  53. rescue URI::InvalidURIError
  54. nil
  55. end
  56. def validate_signature(params)
  57. params = (params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params).with_indifferent_access
  58. return false unless (signature = params[:hmac])
  59. calculated_signature = OpenSSL::HMAC.hexdigest(
  60. OpenSSL::Digest.new('SHA256'), secret, ShopifyAPI::HmacParams.encode(params)
  61. )
  62. Rack::Utils.secure_compare(calculated_signature, signature)
  63. end
  64. private
  65. def extract_current_session
  66. site = ShopifyAPI::Base.site.to_s
  67. token = ShopifyAPI::Base.headers['X-Shopify-Access-Token']
  68. version = ShopifyAPI::Base.api_version
  69. new(domain: site, token: token, api_version: version)
  70. end
  71. end
  72. def initialize(domain:, token:, access_scopes: nil, api_version: ShopifyAPI::Base.api_version, extra: {})
  73. self.domain = self.class.prepare_domain(domain)
  74. self.api_version = api_version
  75. self.token = token
  76. self.access_scopes = access_scopes
  77. self.extra = extra
  78. end
  79. def create_permission_url(scope, redirect_uri, options = {})
  80. params = { client_id: api_key, scope: ShopifyAPI::ApiAccess.new(scope).to_s, redirect_uri: redirect_uri }
  81. params[:state] = options[:state] if options[:state]
  82. params["grant_options[]".to_sym] = options[:grant_options] if options[:grant_options]
  83. construct_oauth_url("authorize", params)
  84. end
  85. def request_token(params)
  86. return token if token
  87. twenty_four_hours_ago = Time.now.utc.to_i - SECONDS_IN_A_DAY
  88. unless self.class.validate_signature(params) && params[:timestamp].to_i > twenty_four_hours_ago
  89. raise ShopifyAPI::ValidationException, "Invalid Signature: Possible malicious login"
  90. end
  91. response = access_token_request(params[:code])
  92. if response.code == "200"
  93. self.extra = JSON.parse(response.body)
  94. self.token = extra.delete('access_token')
  95. if (expires_in = extra.delete('expires_in'))
  96. extra['expires_at'] = Time.now.utc.to_i + expires_in
  97. end
  98. token
  99. else
  100. raise response.msg
  101. end
  102. end
  103. def shop
  104. Shop.current
  105. end
  106. def site
  107. "https://#{domain}"
  108. end
  109. def api_version=(version)
  110. @api_version = if ApiVersion::NullVersion.matches?(version)
  111. ApiVersion::NullVersion
  112. else
  113. ApiVersion.find_version(version)
  114. end
  115. end
  116. def valid?
  117. domain.present? && token.present? && api_version.is_a?(ApiVersion)
  118. end
  119. def expires_in
  120. return unless expires_at.present?
  121. [0, expires_at.to_i - Time.now.utc.to_i].max
  122. end
  123. def expires_at
  124. return unless extra.present?
  125. @expires_at ||= Time.at(extra['expires_at']).utc
  126. end
  127. def expired?
  128. return false if expires_in.nil?
  129. expires_in <= 0
  130. end
  131. def hash
  132. state.hash
  133. end
  134. def ==(other)
  135. self.class == other.class && state == other.state
  136. end
  137. alias_method :eql?, :==
  138. protected
  139. def state
  140. [domain, token, api_version, extra]
  141. end
  142. private
  143. def access_scopes=(access_scopes)
  144. return unless access_scopes
  145. @access_scopes = ShopifyAPI::ApiAccess.new(access_scopes)
  146. end
  147. def parameterize(params)
  148. URI.encode_www_form(params)
  149. end
  150. def access_token_request(code)
  151. uri = URI.parse(construct_oauth_url('access_token'))
  152. https = Net::HTTP.new(uri.host, uri.port)
  153. https.use_ssl = true
  154. request = Net::HTTP::Post.new(uri.request_uri)
  155. request.set_form_data('client_id' => api_key, 'client_secret' => secret, 'code' => code)
  156. https.request(request)
  157. end
  158. def construct_oauth_url(path, query_params = {})
  159. query_string = "?#{parameterize(query_params)}" unless query_params.empty?
  160. "https://#{domain}/admin/oauth/#{path}#{query_string}"
  161. end
  162. end
  163. end