shopify_app 13.0.0 → 14.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +6 -0
  3. data/.github/workflows/rubocop.yml +28 -0
  4. data/.rubocop.yml +13 -6
  5. data/.travis.yml +3 -3
  6. data/CHANGELOG.md +51 -0
  7. data/Gemfile +5 -0
  8. data/README.md +79 -41
  9. data/Rakefile +1 -0
  10. data/SECURITY.md +59 -0
  11. data/app/controllers/concerns/shopify_app/authenticated.rb +1 -0
  12. data/app/controllers/concerns/shopify_app/require_known_shop.rb +39 -0
  13. data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
  14. data/app/controllers/shopify_app/callback_controller.rb +41 -10
  15. data/app/controllers/shopify_app/extension_verification_controller.rb +2 -7
  16. data/app/controllers/shopify_app/sessions_controller.rb +9 -6
  17. data/app/controllers/shopify_app/webhooks_controller.rb +6 -5
  18. data/config/locales/fi.yml +1 -1
  19. data/config/locales/nl.yml +7 -7
  20. data/config/routes.rb +1 -0
  21. data/docs/Quickstart.md +7 -17
  22. data/docs/Releasing.md +1 -0
  23. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +5 -3
  24. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
  25. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +2 -1
  26. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +4 -4
  27. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +5 -4
  28. data/lib/generators/shopify_app/add_webhook/templates/{webhook_job.rb → webhook_job.rb.tt} +5 -0
  29. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +4 -3
  30. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +3 -3
  31. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +10 -9
  32. data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +1 -1
  33. data/lib/generators/shopify_app/controllers/controllers_generator.rb +1 -0
  34. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +22 -3
  35. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +67 -17
  36. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +10 -0
  37. data/lib/generators/shopify_app/install/install_generator.rb +10 -9
  38. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +1 -1
  39. data/lib/generators/shopify_app/install/templates/flash_messages.js +0 -2
  40. data/lib/generators/shopify_app/install/templates/omniauth.rb +2 -1
  41. data/lib/generators/shopify_app/install/templates/{shopify_app.rb → shopify_app.rb.tt} +2 -1
  42. data/lib/generators/shopify_app/install/templates/user_agent.rb +2 -1
  43. data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +19 -0
  44. data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +8 -0
  45. data/lib/generators/shopify_app/routes/routes_generator.rb +1 -0
  46. data/lib/generators/shopify_app/routes/templates/routes.rb +10 -9
  47. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +12 -7
  48. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -0
  49. data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
  50. data/lib/generators/shopify_app/user_model/templates/user.rb +1 -0
  51. data/lib/generators/shopify_app/user_model/user_model_generator.rb +12 -7
  52. data/lib/generators/shopify_app/views/views_generator.rb +1 -0
  53. data/lib/shopify_app/configuration.rb +15 -8
  54. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +3 -3
  55. data/lib/shopify_app/controller_concerns/csrf_protection.rb +15 -0
  56. data/lib/shopify_app/controller_concerns/embedded_app.rb +3 -2
  57. data/lib/shopify_app/controller_concerns/localization.rb +1 -0
  58. data/lib/shopify_app/controller_concerns/login_protection.rb +60 -15
  59. data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
  60. data/lib/shopify_app/controller_concerns/webhook_verification.rb +3 -18
  61. data/lib/shopify_app/engine.rb +5 -0
  62. data/lib/shopify_app/jobs/scripttags_manager_job.rb +1 -1
  63. data/lib/shopify_app/jobs/webhooks_manager_job.rb +1 -1
  64. data/lib/shopify_app/managers/scripttags_manager.rb +4 -3
  65. data/lib/shopify_app/managers/webhooks_manager.rb +4 -3
  66. data/lib/shopify_app/middleware/jwt_middleware.rb +42 -0
  67. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +2 -1
  68. data/lib/shopify_app/session/in_memory_session_store.rb +7 -3
  69. data/lib/shopify_app/session/in_memory_shop_session_store.rb +10 -0
  70. data/lib/shopify_app/session/in_memory_user_session_store.rb +10 -0
  71. data/lib/shopify_app/session/jwt.rb +61 -0
  72. data/lib/shopify_app/session/null_user_session_store.rb +22 -0
  73. data/lib/shopify_app/session/session_repository.rb +13 -16
  74. data/lib/shopify_app/session/session_storage.rb +1 -0
  75. data/lib/shopify_app/session/shop_session_storage.rb +21 -9
  76. data/lib/shopify_app/session/user_session_storage.rb +19 -8
  77. data/lib/shopify_app/test_helpers/all.rb +2 -0
  78. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +17 -0
  79. data/lib/shopify_app/utils.rb +6 -5
  80. data/lib/shopify_app/version.rb +2 -1
  81. data/lib/shopify_app.rb +12 -5
  82. data/package-lock.json +10 -78
  83. data/package.json +1 -1
  84. data/shopify_app.gemspec +12 -7
  85. data/yarn.lock +12 -12
  86. metadata +53 -10
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ProductsController < AuthenticatedController
4
+ def index
5
+ @products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
6
+ render(json: { products: @products })
7
+ end
8
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails/generators/base'
2
3
 
3
4
  module ShopifyApp
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
1
2
 
2
- controller :sessions do
3
- get 'login' => :new, :as => :login
4
- post 'login' => :create, :as => :authenticate
5
- get 'auth/shopify/callback' => :callback
6
- get 'logout' => :destroy, :as => :logout
7
- end
3
+ controller :sessions do
4
+ get 'login' => :new, :as => :login
5
+ post 'login' => :create, :as => :authenticate
6
+ get 'auth/shopify/callback' => :callback
7
+ get 'logout' => :destroy, :as => :logout
8
+ end
8
9
 
9
- namespace :webhooks do
10
- post ':type' => :receive
11
- end
10
+ namespace :webhooks do
11
+ post ':type' => :receive
12
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails/generators/base'
2
3
  require 'rails/generators/active_record'
3
4
 
@@ -8,19 +9,19 @@ module ShopifyApp
8
9
  source_root File.expand_path('../templates', __FILE__)
9
10
 
10
11
  def create_shop_model
11
- copy_file 'shop.rb', 'app/models/shop.rb'
12
+ copy_file('shop.rb', 'app/models/shop.rb')
12
13
  end
13
14
 
14
15
  def create_shop_migration
15
- migration_template 'db/migrate/create_shops.erb', 'db/migrate/create_shops.rb'
16
+ migration_template('db/migrate/create_shops.erb', 'db/migrate/create_shops.rb')
16
17
  end
17
18
 
18
19
  def update_shopify_app_initializer
19
- gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryShopSessionStore', 'Shop'
20
+ gsub_file('config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryShopSessionStore', 'Shop')
20
21
  end
21
22
 
22
23
  def create_shop_fixtures
23
- copy_file 'shops.yml', 'test/fixtures/shops.yml'
24
+ copy_file('shops.yml', 'test/fixtures/shops.yml')
24
25
  end
25
26
 
26
27
  private
@@ -29,9 +30,13 @@ module ShopifyApp
29
30
  Rails.version.match(/\d\.\d/)[0]
30
31
  end
31
32
 
32
- # for generating a timestamp when using `create_migration`
33
- def self.next_migration_number(dir)
34
- ActiveRecord::Generators::Base.next_migration_number(dir)
33
+ class << self
34
+ private :next_migration_number
35
+
36
+ # for generating a timestamp when using `create_migration`
37
+ def next_migration_number(dir)
38
+ ActiveRecord::Generators::Base.next_migration_number(dir)
39
+ end
35
40
  end
36
41
  end
37
42
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Shop < ActiveRecord::Base
2
3
  include ShopifyApp::ShopSessionStorage
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module Generators
3
4
  class ShopifyAppGenerator < Rails::Generators::Base
@@ -7,10 +8,10 @@ module ShopifyApp
7
8
  end
8
9
 
9
10
  def run_all_generators
10
- generate "shopify_app:install #{@opts.join(' ')}"
11
- generate "shopify_app:shop_model"
11
+ generate("shopify_app:install #{@opts.join(' ')}")
12
+ generate("shopify_app:shop_model")
12
13
  generate("shopify_app:authenticated_controller")
13
- generate "shopify_app:home_controller"
14
+ generate("shopify_app:home_controller #{@opts.join(' ')}")
14
15
  end
15
16
  end
16
17
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class User < ActiveRecord::Base
2
3
  include ShopifyApp::UserSessionStorage
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails/generators/base'
2
3
  require 'rails/generators/active_record'
3
4
 
@@ -8,19 +9,19 @@ module ShopifyApp
8
9
  source_root File.expand_path('../templates', __FILE__)
9
10
 
10
11
  def create_user_model
11
- copy_file 'user.rb', 'app/models/user.rb'
12
+ copy_file('user.rb', 'app/models/user.rb')
12
13
  end
13
14
 
14
15
  def create_user_migration
15
- migration_template 'db/migrate/create_users.erb', 'db/migrate/create_users.rb'
16
+ migration_template('db/migrate/create_users.erb', 'db/migrate/create_users.rb')
16
17
  end
17
18
 
18
19
  def update_shopify_app_initializer
19
- gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryUserSessionStore', 'User'
20
+ gsub_file('config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryUserSessionStore', 'User')
20
21
  end
21
22
 
22
23
  def create_user_fixtures
23
- copy_file 'users.yml', 'test/fixtures/users.yml'
24
+ copy_file('users.yml', 'test/fixtures/users.yml')
24
25
  end
25
26
 
26
27
  private
@@ -29,9 +30,13 @@ module ShopifyApp
29
30
  Rails.version.match(/\d\.\d/)[0]
30
31
  end
31
32
 
32
- # for generating a timestamp when using `create_migration`
33
- def self.next_migration_number(dir)
34
- ActiveRecord::Generators::Base.next_migration_number(dir)
33
+ class << self
34
+ private :next_migration_number
35
+
36
+ # for generating a timestamp when using `create_migration`
37
+ def next_migration_number(dir)
38
+ ActiveRecord::Generators::Base.next_migration_number(dir)
39
+ end
35
40
  end
36
41
  end
37
42
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rails/generators/base'
2
3
 
3
4
  module ShopifyApp
@@ -1,11 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class Configuration
3
-
4
4
  # Shopify App settings. These values should match the configuration
5
5
  # for the app in your Shopify Partners page. Change your settings in
6
6
  # `config/initializers/shopify_app.rb`
7
7
  attr_accessor :application_name
8
- attr_accessor :api_key
8
+ attr_accessor :api_key
9
9
  attr_accessor :secret
10
10
  attr_accessor :old_secret
11
11
  attr_accessor :scope
@@ -14,13 +14,11 @@ module ShopifyApp
14
14
  attr_accessor :webhooks
15
15
  attr_accessor :scripttags
16
16
  attr_accessor :after_authenticate_job
17
- attr_reader :shop_session_repository
18
- attr_reader :user_session_repository
19
17
  attr_accessor :api_version
20
18
 
21
19
  # customise urls
22
20
  attr_accessor :root_url
23
- attr_accessor :login_url
21
+ attr_writer :login_url
24
22
 
25
23
  # customise ActiveJob queue names
26
24
  attr_accessor :scripttags_manager_queue_name
@@ -36,7 +34,10 @@ module ShopifyApp
36
34
  attr_accessor :webhook_jobs_namespace
37
35
 
38
36
  # allow enabling of same site none on cookies
39
- attr_accessor :enable_same_site_none
37
+ attr_writer :enable_same_site_none
38
+
39
+ # allow enabling jwt headers for authentication
40
+ attr_accessor :allow_jwt_authentication
40
41
 
41
42
  def initialize
42
43
  @root_url = '/'
@@ -51,15 +52,21 @@ module ShopifyApp
51
52
  end
52
53
 
53
54
  def user_session_repository=(klass)
54
- @user_session_repository = klass
55
55
  ShopifyApp::SessionRepository.user_storage = klass
56
56
  end
57
57
 
58
+ def user_session_repository
59
+ ShopifyApp::SessionRepository.user_storage
60
+ end
61
+
58
62
  def shop_session_repository=(klass)
59
- @shop_session_repository = klass
60
63
  ShopifyApp::SessionRepository.shop_storage = klass
61
64
  end
62
65
 
66
+ def shop_session_repository
67
+ ShopifyApp::SessionRepository.shop_storage
68
+ end
69
+
63
70
  def has_webhooks?
64
71
  webhooks.present?
65
72
  end
@@ -1,14 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module AppProxyVerification
3
4
  extend ActiveSupport::Concern
4
-
5
5
  included do
6
6
  skip_before_action :verify_authenticity_token, raise: false
7
7
  before_action :verify_proxy_request
8
8
  end
9
9
 
10
10
  def verify_proxy_request
11
- return head :forbidden unless query_string_valid?(request.query_string)
11
+ return head(:forbidden) unless query_string_valid?(request.query_string)
12
12
  end
13
13
 
14
14
  private
@@ -26,7 +26,7 @@ module ShopifyApp
26
26
  end
27
27
 
28
28
  def calculated_signature(query_hash_without_signature)
29
- sorted_params = query_hash_without_signature.collect{|k,v| "#{k}=#{Array(v).join(',')}"}.sort.join
29
+ sorted_params = query_hash_without_signature.collect { |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join
30
30
 
31
31
  OpenSSL::HMAC.hexdigest(
32
32
  OpenSSL::Digest.new('sha256'),
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ module CsrfProtection
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ protect_from_forgery with: :exception, unless: :valid_session_token?
7
+ end
8
+
9
+ private
10
+
11
+ def valid_session_token?
12
+ request.env['jwt.shopify_domain']
13
+ end
14
+ end
15
+ end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module EmbeddedApp
3
4
  extend ActiveSupport::Concern
4
5
 
5
6
  included do
6
7
  if ShopifyApp.configuration.embedded_app?
7
- after_action :set_esdk_headers
8
- layout 'embedded_app'
8
+ after_action(:set_esdk_headers)
9
+ layout('embedded_app')
9
10
  end
10
11
  end
11
12
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module Localization
3
4
  extend ActiveSupport::Concern
@@ -11,9 +11,11 @@ module ShopifyApp
11
11
 
12
12
  included do
13
13
  after_action :set_test_cookie
14
- rescue_from ActiveResource::UnauthorizedAccess, :with => :close_session
14
+ rescue_from ActiveResource::UnauthorizedAccess, with: :close_session
15
15
  end
16
16
 
17
+ ACCESS_TOKEN_REQUIRED_HEADER = 'X-Shopify-API-Request-Failure-Unauthorized'
18
+
17
19
  def activate_shopify_session
18
20
  return redirect_to_login if current_shopify_session.blank?
19
21
  clear_top_level_oauth_cookie
@@ -27,20 +29,38 @@ module ShopifyApp
27
29
  end
28
30
 
29
31
  def current_shopify_session
30
- if session[:user_id].present?
31
- @current_shopify_session ||= user_session
32
- else
33
- @current_shopify_session ||= shop_session
32
+ @current_shopify_session ||= begin
33
+ user_session || shop_session
34
34
  end
35
35
  end
36
36
 
37
37
  def user_session
38
- return if session[:user_id].blank?
38
+ user_session_by_jwt || user_session_by_cookie
39
+ end
40
+
41
+ def user_session_by_jwt
42
+ return unless ShopifyApp.configuration.allow_jwt_authentication
43
+ return unless jwt_shopify_user_id
44
+ ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(jwt_shopify_user_id)
45
+ end
46
+
47
+ def user_session_by_cookie
48
+ return unless session[:user_id].present?
39
49
  ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
40
50
  end
41
51
 
42
52
  def shop_session
43
- return if session[:shop_id].blank?
53
+ shop_session_by_jwt || shop_session_by_cookie
54
+ end
55
+
56
+ def shop_session_by_jwt
57
+ return unless ShopifyApp.configuration.allow_jwt_authentication
58
+ return unless jwt_shopify_domain
59
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(jwt_shopify_domain)
60
+ end
61
+
62
+ def shop_session_by_cookie
63
+ return unless session[:shop_id].present?
44
64
  ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
45
65
  end
46
66
 
@@ -50,7 +70,9 @@ module ShopifyApp
50
70
 
51
71
  end
52
72
 
53
- if current_shopify_session && params[:shop] && params[:shop].is_a?(String) && (current_shopify_session.domain != params[:shop])
73
+ if current_shopify_session &&
74
+ params[:shop] && params[:shop].is_a?(String) &&
75
+ (current_shopify_session.domain != params[:shop])
54
76
  clear_session = true
55
77
  end
56
78
 
@@ -60,11 +82,23 @@ module ShopifyApp
60
82
  end
61
83
  end
62
84
 
85
+ def signal_access_token_required
86
+ response.set_header(ACCESS_TOKEN_REQUIRED_HEADER, true)
87
+ end
88
+
63
89
  protected
64
90
 
91
+ def jwt_shopify_domain
92
+ request.env['jwt.shopify_domain']
93
+ end
94
+
95
+ def jwt_shopify_user_id
96
+ request.env['jwt.shopify_user_id']
97
+ end
98
+
65
99
  def redirect_to_login
66
100
  if request.xhr?
67
- head :unauthorized
101
+ head(:unauthorized)
68
102
  else
69
103
  if request.get?
70
104
  path = request.path
@@ -74,7 +108,7 @@ module ShopifyApp
74
108
  path = referer.path
75
109
  query = "#{referer.query}&#{sanitized_params.to_query}"
76
110
  end
77
- session[:return_to] = "#{path}?#{query}"
111
+ session[:return_to] = query.blank? ? path.to_s : "#{path}?#{query}"
78
112
  redirect_to(login_url_with_optional_shop)
79
113
  end
80
114
  end
@@ -105,7 +139,7 @@ module ShopifyApp
105
139
  query_params = {}
106
140
  query_params[:shop] = sanitized_params[:shop] if params[:shop].present?
107
141
 
108
- return_to = session[:return_to] || params[:return_to]
142
+ return_to = RedirectSafely.make_safe(session[:return_to] || params[:return_to], nil)
109
143
 
110
144
  if return_to.present? && return_to_param_required?
111
145
  query_params[:return_to] = return_to
@@ -128,14 +162,18 @@ module ShopifyApp
128
162
 
129
163
  def fullpage_redirect_to(url)
130
164
  if ShopifyApp.configuration.embedded_app?
131
- render 'shopify_app/shared/redirect', layout: false, locals: { url: url, current_shopify_domain: current_shopify_domain }
165
+ render('shopify_app/shared/redirect', layout: false,
166
+ locals: { url: url, current_shopify_domain: current_shopify_domain })
132
167
  else
133
- redirect_to url
168
+ redirect_to(url)
134
169
  end
135
170
  end
136
171
 
137
172
  def current_shopify_domain
138
- shopify_domain = sanitized_shop_name || session[:shopify_domain]
173
+ shopify_domain = sanitized_shop_name ||
174
+ jwt_shopify_domain ||
175
+ session[:shopify_domain]
176
+
139
177
  return shopify_domain if shopify_domain.present?
140
178
 
141
179
  raise ShopifyDomainNotFound
@@ -170,11 +208,18 @@ module ShopifyApp
170
208
  end
171
209
 
172
210
  def return_address
211
+ return base_return_address unless ShopifyApp.configuration.allow_jwt_authentication
212
+ return_address_with_params(shop: current_shopify_domain)
213
+ rescue ShopifyDomainNotFound
214
+ base_return_address
215
+ end
216
+
217
+ def base_return_address
173
218
  session.delete(:return_to) || ShopifyApp.configuration.root_url
174
219
  end
175
220
 
176
221
  def return_address_with_params(params)
177
- uri = URI(return_address)
222
+ uri = URI(base_return_address)
178
223
  uri.query = CGI.parse(uri.query.to_s)
179
224
  .symbolize_keys
180
225
  .transform_values { |v| v.one? ? v.first : v }