shopify_app 13.0.0 → 13.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -6
  3. data/CHANGELOG.md +10 -0
  4. data/Gemfile +3 -0
  5. data/README.md +32 -1
  6. data/Rakefile +1 -0
  7. data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
  8. data/app/controllers/shopify_app/callback_controller.rb +1 -1
  9. data/app/controllers/shopify_app/sessions_controller.rb +8 -5
  10. data/app/controllers/shopify_app/webhooks_controller.rb +6 -5
  11. data/config/locales/fi.yml +1 -1
  12. data/config/routes.rb +1 -0
  13. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +5 -3
  14. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
  15. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +2 -1
  16. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +4 -4
  17. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +5 -4
  18. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb +5 -0
  19. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +4 -3
  20. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +3 -3
  21. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +10 -9
  22. data/lib/generators/shopify_app/controllers/controllers_generator.rb +1 -0
  23. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +4 -3
  24. data/lib/generators/shopify_app/install/install_generator.rb +9 -8
  25. data/lib/generators/shopify_app/install/templates/omniauth.rb +2 -1
  26. data/lib/generators/shopify_app/install/templates/user_agent.rb +2 -1
  27. data/lib/generators/shopify_app/routes/routes_generator.rb +1 -0
  28. data/lib/generators/shopify_app/routes/templates/routes.rb +10 -9
  29. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +5 -4
  30. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -0
  31. data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
  32. data/lib/generators/shopify_app/user_model/templates/user.rb +1 -0
  33. data/lib/generators/shopify_app/user_model/user_model_generator.rb +5 -4
  34. data/lib/generators/shopify_app/views/views_generator.rb +1 -0
  35. data/lib/shopify_app.rb +7 -4
  36. data/lib/shopify_app/configuration.rb +15 -8
  37. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +3 -2
  38. data/lib/shopify_app/controller_concerns/embedded_app.rb +3 -2
  39. data/lib/shopify_app/controller_concerns/localization.rb +1 -0
  40. data/lib/shopify_app/controller_concerns/login_protection.rb +46 -11
  41. data/lib/shopify_app/controller_concerns/webhook_verification.rb +2 -1
  42. data/lib/shopify_app/engine.rb +1 -0
  43. data/lib/shopify_app/jobs/scripttags_manager_job.rb +1 -1
  44. data/lib/shopify_app/jobs/webhooks_manager_job.rb +1 -1
  45. data/lib/shopify_app/managers/scripttags_manager.rb +4 -3
  46. data/lib/shopify_app/managers/webhooks_manager.rb +4 -3
  47. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +2 -1
  48. data/lib/shopify_app/session/in_memory_session_store.rb +7 -3
  49. data/lib/shopify_app/session/in_memory_shop_session_store.rb +10 -0
  50. data/lib/shopify_app/session/in_memory_user_session_store.rb +10 -0
  51. data/lib/shopify_app/session/jwt.rb +48 -0
  52. data/lib/shopify_app/session/null_user_session_store.rb +22 -0
  53. data/lib/shopify_app/session/session_repository.rb +13 -16
  54. data/lib/shopify_app/session/session_storage.rb +1 -0
  55. data/lib/shopify_app/session/shop_session_storage.rb +21 -9
  56. data/lib/shopify_app/session/user_session_storage.rb +19 -8
  57. data/lib/shopify_app/test_helpers/all.rb +1 -0
  58. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +16 -0
  59. data/lib/shopify_app/utils.rb +6 -5
  60. data/lib/shopify_app/version.rb +2 -1
  61. data/package-lock.json +4 -4
  62. data/package.json +1 -1
  63. data/shopify_app.gemspec +8 -4
  64. data/yarn.lock +3 -3
  65. metadata +22 -3
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class WebhooksManagerJob < ActiveJob::Base
3
-
4
4
  queue_as do
5
5
  ShopifyApp.configuration.webhooks_manager_queue_name
6
6
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class ScripttagsManager
3
4
  class CreationFailed < StandardError; end
@@ -43,7 +44,7 @@ module ShopifyApp
43
44
  def destroy_scripttags
44
45
  scripttags = expanded_scripttags
45
46
  ShopifyAPI::ScriptTag.all.each do |tag|
46
- ShopifyAPI::ScriptTag.delete(tag.id) if is_required_scripttag?(scripttags, tag)
47
+ ShopifyAPI::ScriptTag.delete(tag.id) if required_scripttag?(scripttags, tag)
47
48
  end
48
49
 
49
50
  @current_scripttags = nil
@@ -55,8 +56,8 @@ module ShopifyApp
55
56
  self.class.build_src(required_scripttags, shop_domain)
56
57
  end
57
58
 
58
- def is_required_scripttag?(scripttags, tag)
59
- scripttags.map{ |w| w[:src] }.include? tag.src
59
+ def required_scripttag?(scripttags, tag)
60
+ scripttags.map { |w| w[:src] }.include?(tag.src)
60
61
  end
61
62
 
62
63
  def create_scripttag(attributes)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class WebhooksManager
3
4
  class CreationFailed < StandardError; end
@@ -31,7 +32,7 @@ module ShopifyApp
31
32
 
32
33
  def destroy_webhooks
33
34
  ShopifyAPI::Webhook.all.to_a.each do |webhook|
34
- ShopifyAPI::Webhook.delete(webhook.id) if is_required_webhook?(webhook)
35
+ ShopifyAPI::Webhook.delete(webhook.id) if required_webhook?(webhook)
35
36
  end
36
37
 
37
38
  @current_webhooks = nil
@@ -39,8 +40,8 @@ module ShopifyApp
39
40
 
40
41
  private
41
42
 
42
- def is_required_webhook?(webhook)
43
- required_webhooks.map{ |w| w[:address] }.include? webhook.address
43
+ def required_webhook?(webhook)
44
+ required_webhooks.map { |w| w[:address] }.include?(webhook.address)
44
45
  end
45
46
 
46
47
  def create_webhook(attributes)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class SameSiteCookieMiddleware
3
4
  COOKIE_SEPARATOR = "\n"
@@ -19,7 +20,7 @@ module ShopifyApp
19
20
  .split(COOKIE_SEPARATOR)
20
21
  .compact
21
22
  .map do |cookie|
22
- cookie << '; Secure' if not cookie =~ /;\s*secure/i
23
+ cookie << '; Secure' unless cookie =~ /;\s*secure/i
23
24
  cookie << '; SameSite=None' unless cookie =~ /;\s*samesite=/i
24
25
  cookie
25
26
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
3
+ # rubocop:disable Style/ClassVars
4
+ # Class var repo is needed here in order to share data between the 2 child classes.
2
5
  class InMemorySessionStore
3
6
  class EnvironmentError < StandardError; end
4
7
 
@@ -6,7 +9,7 @@ module ShopifyApp
6
9
  repo[id]
7
10
  end
8
11
 
9
- def self.store(session, *args)
12
+ def self.store(session, *_args)
10
13
  id = SecureRandom.uuid
11
14
  repo[id] = session
12
15
  id
@@ -18,10 +21,11 @@ module ShopifyApp
18
21
 
19
22
  def self.repo
20
23
  if Rails.env.production?
21
- raise EnvironmentError.new("Cannot use InMemorySessionStore in a Production environment. \
22
- Please initialize ShopifyApp with a model that can store and retrieve sessions")
24
+ raise EnvironmentError, "Cannot use InMemorySessionStore in a Production environment. \
25
+ Please initialize ShopifyApp with a model that can store and retrieve sessions"
23
26
  end
24
27
  @@repo ||= {}
25
28
  end
26
29
  end
30
+ # rubocop:enable Style/ClassVars
27
31
  end
@@ -1,4 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class InMemoryShopSessionStore < InMemorySessionStore
4
+ def self.store(session, *args)
5
+ id = super
6
+ repo[session.domain] = session
7
+ id
8
+ end
9
+
10
+ def self.retrieve_by_shopify_domain(shopify_domain)
11
+ repo[shopify_domain]
12
+ end
3
13
  end
4
14
  end
@@ -1,4 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class InMemoryUserSessionStore < InMemorySessionStore
4
+ def self.store(session, user)
5
+ id = super
6
+ repo[user.shopify_user_id] = session
7
+ id
8
+ end
9
+
10
+ def self.retrieve_by_shopify_user_id(user_id)
11
+ repo[user_id]
12
+ end
3
13
  end
4
14
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class JWT
4
+ def initialize(token)
5
+ @token = token
6
+ set_payload
7
+ end
8
+
9
+ def shopify_domain
10
+ payload && dest_host
11
+ end
12
+
13
+ def shopify_user_id
14
+ payload && payload['sub']
15
+ end
16
+
17
+ private
18
+
19
+ def payload
20
+ return unless @payload
21
+ return unless dest_host
22
+ return unless dest_host == iss_host
23
+ return unless @payload['aud'] == ShopifyApp.configuration.api_key
24
+
25
+ @payload
26
+ end
27
+
28
+ def set_payload
29
+ @payload, _ = parse_token_data(ShopifyApp.configuration.secret)
30
+ configuration_old_secret = @payload && ShopifyApp.configuration
31
+ @payload, _ = parse_token_data(ShopifyApp.configuration.old_secret) unless configuration_old_secret
32
+ end
33
+
34
+ def parse_token_data(secret)
35
+ ::JWT.decode(@token, secret, true, { algorithm: 'HS256' })
36
+ rescue ::JWT::DecodeError, ::JWT::VerificationError, ::JWT::ExpiredSignature, ::JWT::ImmatureSignature
37
+ nil
38
+ end
39
+
40
+ def dest_host
41
+ @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['dest'])
42
+ end
43
+
44
+ def iss_host
45
+ @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['iss'])
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class NullUserSessionStore
4
+ class << self
5
+ def retrieve(_)
6
+ nil
7
+ end
8
+
9
+ def store(_, _)
10
+ raise SessionRepository::ConfigurationError, 'user_storage is not configured'
11
+ end
12
+
13
+ def retrieve_by_shopify_user_id(_)
14
+ nil
15
+ end
16
+
17
+ def blank?
18
+ true
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,23 +1,12 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class SessionRepository
3
4
  class ConfigurationError < StandardError; end
4
5
 
5
6
  class << self
6
- def shop_storage=(storage)
7
- @shop_storage = storage
7
+ attr_writer :shop_storage
8
8
 
9
- unless storage.nil? || self.shop_storage.respond_to?(:store) && self.shop_storage.respond_to?(:retrieve)
10
- raise ArgumentError, "shop storage must respond to :store and :retrieve"
11
- end
12
- end
13
-
14
- def user_storage=(storage)
15
- @user_storage = storage
16
-
17
- unless storage.nil? || self.user_storage.respond_to?(:store) && self.user_storage.respond_to?(:retrieve)
18
- raise ArgumentError, "user storage must respond to :store and :retrieve"
19
- end
20
- end
9
+ attr_writer :user_storage
21
10
 
22
11
  def retrieve_shop_session(id)
23
12
  shop_storage.retrieve(id)
@@ -27,6 +16,14 @@ module ShopifyApp
27
16
  user_storage.retrieve(id)
28
17
  end
29
18
 
19
+ def retrieve_shop_session_by_shopify_domain(shopify_domain)
20
+ shop_storage.retrieve_by_shopify_domain(shopify_domain)
21
+ end
22
+
23
+ def retrieve_user_session_by_shopify_user_id(user_id)
24
+ user_storage.retrieve_by_shopify_user_id(user_id)
25
+ end
26
+
30
27
  def store_shop_session(session)
31
28
  shop_storage.store(session)
32
29
  end
@@ -36,7 +33,7 @@ module ShopifyApp
36
33
  end
37
34
 
38
35
  def shop_storage
39
- load_shop_storage || raise(ConfigurationError.new("ShopifySessionRepository.shop_storage is not configured!"))
36
+ load_shop_storage || raise(ConfigurationError, "ShopifySessionRepository.shop_storage is not configured!")
40
37
  end
41
38
 
42
39
  def user_storage
@@ -51,7 +48,7 @@ module ShopifyApp
51
48
  end
52
49
 
53
50
  def load_user_storage
54
- return unless @user_storage
51
+ return NullUserSessionStore unless @user_storage
55
52
  @user_storage.respond_to?(:safe_constantize) ? @user_storage.safe_constantize : @user_storage
56
53
  end
57
54
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module SessionStorage
3
4
  extend ActiveSupport::Concern
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module ShopSessionStorage
3
4
  extend ActiveSupport::Concern
@@ -8,7 +9,7 @@ module ShopifyApp
8
9
  end
9
10
 
10
11
  class_methods do
11
- def store(auth_session, *args)
12
+ def store(auth_session, *_args)
12
13
  shop = find_or_initialize_by(shopify_domain: auth_session.domain)
13
14
  shop.shopify_token = auth_session.token
14
15
  shop.save!
@@ -16,14 +17,25 @@ module ShopifyApp
16
17
  end
17
18
 
18
19
  def retrieve(id)
19
- return unless id
20
- if shop = self.find_by(id: id)
21
- ShopifyAPI::Session.new(
22
- domain: shop.shopify_domain,
23
- token: shop.shopify_token,
24
- api_version: shop.api_version
25
- )
26
- end
20
+ shop = find_by(id: id)
21
+ construct_session(shop)
22
+ end
23
+
24
+ def retrieve_by_shopify_domain(domain)
25
+ shop = find_by(shopify_domain: domain)
26
+ construct_session(shop)
27
+ end
28
+
29
+ private
30
+
31
+ def construct_session(shop)
32
+ return unless shop
33
+
34
+ ShopifyAPI::Session.new(
35
+ domain: shop.shopify_domain,
36
+ token: shop.shopify_token,
37
+ api_version: shop.api_version,
38
+ )
27
39
  end
28
40
  end
29
41
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module UserSessionStorage
3
4
  extend ActiveSupport::Concern
@@ -17,14 +18,24 @@ module ShopifyApp
17
18
  end
18
19
 
19
20
  def retrieve(id)
20
- return unless id
21
- if user = find_by(id: id)
22
- ShopifyAPI::Session.new(
23
- domain: user.shopify_domain,
24
- token: user.shopify_token,
25
- api_version: user.api_version
26
- )
27
- end
21
+ user = find_by(id: id)
22
+ construct_session(user)
23
+ end
24
+
25
+ def retrieve_by_shopify_user_id(user_id)
26
+ user = find_by(shopify_user_id: user_id)
27
+ construct_session(user)
28
+ end
29
+
30
+ private
31
+
32
+ def construct_session(user)
33
+ return unless user
34
+ ShopifyAPI::Session.new(
35
+ domain: user.shopify_domain,
36
+ token: user.shopify_token,
37
+ api_version: user.api_version,
38
+ )
28
39
  end
29
40
  end
30
41
  end
@@ -0,0 +1 @@
1
+ require 'shopify_app/test_helpers/webhook_verification_helper'
@@ -0,0 +1,16 @@
1
+ module ShopifyApp
2
+ module TestHelpers
3
+ module WebhookVerificationHelper
4
+ def authorized_webhook_verification_headers!(params = {})
5
+ digest = OpenSSL::Digest.new('sha256')
6
+ secret = ShopifyApp.configuration.secret
7
+ valid_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, params.to_query)).strip
8
+ @request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] = valid_hmac
9
+ end
10
+
11
+ def unauthorized_webhook_verification_headers!(params = {})
12
+ @request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] = "invalid_hmac"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,13 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module Utils
3
-
4
4
  def self.sanitize_shop_domain(shop_domain)
5
+ myshopify_domain = ShopifyApp.configuration.myshopify_domain
5
6
  name = shop_domain.to_s.downcase.strip
6
- name += ".#{ShopifyApp.configuration.myshopify_domain}" if !name.include?("#{ShopifyApp.configuration.myshopify_domain}") && !name.include?(".")
7
+ name += ".#{myshopify_domain}" if !name.include?(myshopify_domain.to_s) && !name.include?(".")
7
8
  name.sub!(%r|https?://|, '')
8
9
 
9
10
  u = URI("http://#{name}")
10
- u.host if u.host&.match(/^[a-z0-9][a-z0-9\-]*[a-z0-9]\.#{Regexp.escape(ShopifyApp.configuration.myshopify_domain)}$/)
11
+ u.host if u.host&.match(/^[a-z0-9][a-z0-9\-]*[a-z0-9]\.#{Regexp.escape(myshopify_domain)}$/)
11
12
  rescue URI::InvalidURIError
12
13
  nil
13
14
  end
@@ -16,8 +17,8 @@ module ShopifyApp
16
17
  Rails.logger.info("[ShopifyAPI::ApiVersion] Fetching known Admin API Versions from Shopify...")
17
18
  ShopifyAPI::ApiVersion.fetch_known_versions
18
19
  Rails.logger.info("[ShopifyAPI::ApiVersion] Known API Versions: #{ShopifyAPI::ApiVersion.versions.keys}")
19
- rescue ActiveResource::ConnectionError
20
- logger.error( "[ShopifyAPI::ApiVersion] Unable to fetch api_versions from Shopify")
20
+ rescue ActiveResource::ConnectionError
21
+ logger.error("[ShopifyAPI::ApiVersion] Unable to fetch api_versions from Shopify")
21
22
  end
22
23
  end
23
24
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
- VERSION = '13.0.0'.freeze
3
+ VERSION = '13.0.1'
3
4
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify_app",
3
- "version": "12.0.2",
3
+ "version": "13.0.0",
4
4
  "lockfileVersion": 1,
5
5
  "requires": true,
6
6
  "dependencies": {
@@ -1332,9 +1332,9 @@
1332
1332
  }
1333
1333
  },
1334
1334
  "acorn": {
1335
- "version": "6.3.0",
1336
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
1337
- "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
1335
+ "version": "6.4.1",
1336
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
1337
+ "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
1338
1338
  "dev": true
1339
1339
  },
1340
1340
  "after": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify_app",
3
- "version": "13.0.0",
3
+ "version": "13.0.1",
4
4
  "repository": "git@github.com:Shopify/shopify_app.git",
5
5
  "author": "Shopify",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+ $LOAD_PATH.push(File.expand_path('../lib', __FILE__))
2
3
  require "shopify_app/version"
3
4
 
4
5
  Gem::Specification.new do |s|
@@ -6,14 +7,17 @@ Gem::Specification.new do |s|
6
7
  s.version = ShopifyApp::VERSION
7
8
  s.platform = Gem::Platform::RUBY
8
9
  s.author = "Shopify"
9
- s.summary = %q{This gem is used to get quickly started with the Shopify API}
10
+ s.summary = 'This gem is used to get quickly started with the Shopify API'
10
11
 
11
12
  s.required_ruby_version = ">= 2.3.1"
12
13
 
14
+ s.metadata['allowed_push_host'] = 'https://rubygems.org'
15
+
13
16
  s.add_runtime_dependency('browser_sniffer', '~> 1.2.0')
14
17
  s.add_runtime_dependency('rails', '> 5.2.1')
15
18
  s.add_runtime_dependency('shopify_api', '~> 9.0.2')
16
19
  s.add_runtime_dependency('omniauth-shopify-oauth2', '~> 2.2.2')
20
+ s.add_runtime_dependency('jwt', '~> 2.2.1')
17
21
 
18
22
  s.add_development_dependency('rake')
19
23
  s.add_development_dependency('byebug')
@@ -26,7 +30,7 @@ Gem::Specification.new do |s|
26
30
  s.add_development_dependency('mocha')
27
31
  s.add_development_dependency('webmock')
28
32
 
29
- s.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^(test|example)/}) }
30
- s.test_files = `git ls-files -- {test}/*`.split("\n")
33
+ s.files = %x(git ls-files).split("\n").reject { |f| f.match(%r{^(test|example)/}) }
34
+ s.test_files = %x(git ls-files -- {test}/*).split("\n")
31
35
  s.require_paths = ["lib"]
32
36
  end