shopify_app 13.0.0 → 13.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/.github/workflows/rubocop.yml +28 -0
 - data/.rubocop.yml +13 -6
 - data/.travis.yml +4 -3
 - data/CHANGELOG.md +33 -0
 - data/Gemfile +5 -0
 - data/README.md +67 -38
 - data/Rakefile +1 -0
 - data/SECURITY.md +59 -0
 - data/app/controllers/concerns/shopify_app/require_known_shop.rb +39 -0
 - data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
 - data/app/controllers/shopify_app/callback_controller.rb +39 -8
 - data/app/controllers/shopify_app/extension_verification_controller.rb +2 -7
 - data/app/controllers/shopify_app/sessions_controller.rb +9 -6
 - data/app/controllers/shopify_app/webhooks_controller.rb +6 -5
 - data/config/locales/fi.yml +1 -1
 - data/config/locales/nl.yml +7 -7
 - data/config/routes.rb +1 -0
 - data/docs/Quickstart.md +5 -14
 - data/docs/Releasing.md +1 -0
 - data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +5 -3
 - data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
 - data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +2 -1
 - data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +4 -4
 - data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +5 -4
 - data/lib/generators/shopify_app/add_webhook/templates/{webhook_job.rb → webhook_job.rb.tt} +5 -0
 - data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +4 -3
 - data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +3 -3
 - data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +10 -9
 - data/lib/generators/shopify_app/controllers/controllers_generator.rb +1 -0
 - data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +4 -3
 - data/lib/generators/shopify_app/install/install_generator.rb +10 -9
 - data/lib/generators/shopify_app/install/templates/omniauth.rb +2 -1
 - data/lib/generators/shopify_app/install/templates/{shopify_app.rb → shopify_app.rb.tt} +1 -1
 - data/lib/generators/shopify_app/install/templates/user_agent.rb +2 -1
 - data/lib/generators/shopify_app/routes/routes_generator.rb +1 -0
 - data/lib/generators/shopify_app/routes/templates/routes.rb +10 -9
 - data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +12 -7
 - data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -0
 - data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
 - data/lib/generators/shopify_app/user_model/templates/user.rb +1 -0
 - data/lib/generators/shopify_app/user_model/user_model_generator.rb +12 -7
 - data/lib/generators/shopify_app/views/views_generator.rb +1 -0
 - data/lib/shopify_app.rb +11 -5
 - data/lib/shopify_app/configuration.rb +15 -8
 - data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +3 -3
 - data/lib/shopify_app/controller_concerns/embedded_app.rb +3 -2
 - data/lib/shopify_app/controller_concerns/localization.rb +1 -0
 - data/lib/shopify_app/controller_concerns/login_protection.rb +52 -15
 - data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
 - data/lib/shopify_app/controller_concerns/webhook_verification.rb +3 -18
 - data/lib/shopify_app/engine.rb +5 -0
 - data/lib/shopify_app/jobs/scripttags_manager_job.rb +1 -1
 - data/lib/shopify_app/jobs/webhooks_manager_job.rb +1 -1
 - data/lib/shopify_app/managers/scripttags_manager.rb +4 -3
 - data/lib/shopify_app/managers/webhooks_manager.rb +4 -3
 - data/lib/shopify_app/middleware/jwt_middleware.rb +42 -0
 - data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +2 -1
 - data/lib/shopify_app/session/in_memory_session_store.rb +7 -3
 - data/lib/shopify_app/session/in_memory_shop_session_store.rb +10 -0
 - data/lib/shopify_app/session/in_memory_user_session_store.rb +10 -0
 - data/lib/shopify_app/session/jwt.rb +61 -0
 - data/lib/shopify_app/session/null_user_session_store.rb +22 -0
 - data/lib/shopify_app/session/session_repository.rb +13 -16
 - data/lib/shopify_app/session/session_storage.rb +1 -0
 - data/lib/shopify_app/session/shop_session_storage.rb +21 -9
 - data/lib/shopify_app/session/user_session_storage.rb +19 -8
 - data/lib/shopify_app/test_helpers/all.rb +2 -0
 - data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +17 -0
 - data/lib/shopify_app/utils.rb +6 -5
 - data/lib/shopify_app/version.rb +2 -1
 - data/package-lock.json +4 -4
 - data/package.json +1 -1
 - data/shopify_app.gemspec +12 -7
 - data/yarn.lock +3 -3
 - metadata +48 -10
 
| 
         @@ -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 
     | 
| 
      
 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'),
         
     | 
| 
         @@ -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 
     | 
| 
       8 
     | 
    
         
            -
                    layout 
     | 
| 
      
 8 
     | 
    
         
            +
                    after_action(:set_esdk_headers)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    layout('embedded_app')
         
     | 
| 
       9 
10 
     | 
    
         
             
                  end
         
     | 
| 
       10 
11 
     | 
    
         
             
                end
         
     | 
| 
       11 
12 
     | 
    
         | 
| 
         @@ -11,7 +11,7 @@ module ShopifyApp 
     | 
|
| 
       11 
11 
     | 
    
         | 
| 
       12 
12 
     | 
    
         
             
                included do
         
     | 
| 
       13 
13 
     | 
    
         
             
                  after_action :set_test_cookie
         
     | 
| 
       14 
     | 
    
         
            -
                  rescue_from ActiveResource::UnauthorizedAccess, : 
     | 
| 
      
 14 
     | 
    
         
            +
                  rescue_from ActiveResource::UnauthorizedAccess, with: :close_session
         
     | 
| 
       15 
15 
     | 
    
         
             
                end
         
     | 
| 
       16 
16 
     | 
    
         | 
| 
       17 
17 
     | 
    
         
             
                def activate_shopify_session
         
     | 
| 
         @@ -27,20 +27,38 @@ module ShopifyApp 
     | 
|
| 
       27 
27 
     | 
    
         
             
                end
         
     | 
| 
       28 
28 
     | 
    
         | 
| 
       29 
29 
     | 
    
         
             
                def current_shopify_session
         
     | 
| 
       30 
     | 
    
         
            -
                   
     | 
| 
       31 
     | 
    
         
            -
                     
     | 
| 
       32 
     | 
    
         
            -
                  else
         
     | 
| 
       33 
     | 
    
         
            -
                    @current_shopify_session ||= shop_session
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @current_shopify_session ||= begin
         
     | 
| 
      
 31 
     | 
    
         
            +
                    user_session || shop_session
         
     | 
| 
       34 
32 
     | 
    
         
             
                  end
         
     | 
| 
       35 
33 
     | 
    
         
             
                end
         
     | 
| 
       36 
34 
     | 
    
         | 
| 
       37 
35 
     | 
    
         
             
                def user_session
         
     | 
| 
       38 
     | 
    
         
            -
                   
     | 
| 
      
 36 
     | 
    
         
            +
                  user_session_by_jwt || user_session_by_cookie
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                def user_session_by_jwt
         
     | 
| 
      
 40 
     | 
    
         
            +
                  return unless ShopifyApp.configuration.allow_jwt_authentication
         
     | 
| 
      
 41 
     | 
    
         
            +
                  return unless jwt_shopify_user_id
         
     | 
| 
      
 42 
     | 
    
         
            +
                  ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(jwt_shopify_user_id)
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                def user_session_by_cookie
         
     | 
| 
      
 46 
     | 
    
         
            +
                  return unless session[:user_id].present?
         
     | 
| 
       39 
47 
     | 
    
         
             
                  ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
         
     | 
| 
       40 
48 
     | 
    
         
             
                end
         
     | 
| 
       41 
49 
     | 
    
         | 
| 
       42 
50 
     | 
    
         
             
                def shop_session
         
     | 
| 
       43 
     | 
    
         
            -
                   
     | 
| 
      
 51 
     | 
    
         
            +
                  shop_session_by_jwt || shop_session_by_cookie
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                def shop_session_by_jwt
         
     | 
| 
      
 55 
     | 
    
         
            +
                  return unless ShopifyApp.configuration.allow_jwt_authentication
         
     | 
| 
      
 56 
     | 
    
         
            +
                  return unless jwt_shopify_domain
         
     | 
| 
      
 57 
     | 
    
         
            +
                  ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(jwt_shopify_domain)
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                def shop_session_by_cookie
         
     | 
| 
      
 61 
     | 
    
         
            +
                  return unless session[:shop_id].present?
         
     | 
| 
       44 
62 
     | 
    
         
             
                  ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
         
     | 
| 
       45 
63 
     | 
    
         
             
                end
         
     | 
| 
       46 
64 
     | 
    
         | 
| 
         @@ -50,7 +68,9 @@ module ShopifyApp 
     | 
|
| 
       50 
68 
     | 
    
         | 
| 
       51 
69 
     | 
    
         
             
                  end
         
     | 
| 
       52 
70 
     | 
    
         | 
| 
       53 
     | 
    
         
            -
                  if current_shopify_session && 
     | 
| 
      
 71 
     | 
    
         
            +
                  if current_shopify_session &&
         
     | 
| 
      
 72 
     | 
    
         
            +
                    params[:shop] && params[:shop].is_a?(String) &&
         
     | 
| 
      
 73 
     | 
    
         
            +
                    (current_shopify_session.domain != params[:shop])
         
     | 
| 
       54 
74 
     | 
    
         
             
                    clear_session = true
         
     | 
| 
       55 
75 
     | 
    
         
             
                  end
         
     | 
| 
       56 
76 
     | 
    
         | 
| 
         @@ -62,9 +82,17 @@ module ShopifyApp 
     | 
|
| 
       62 
82 
     | 
    
         | 
| 
       63 
83 
     | 
    
         
             
                protected
         
     | 
| 
       64 
84 
     | 
    
         | 
| 
      
 85 
     | 
    
         
            +
                def jwt_shopify_domain
         
     | 
| 
      
 86 
     | 
    
         
            +
                  request.env['jwt.shopify_domain']
         
     | 
| 
      
 87 
     | 
    
         
            +
                end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                def jwt_shopify_user_id
         
     | 
| 
      
 90 
     | 
    
         
            +
                  request.env['jwt.shopify_user_id']
         
     | 
| 
      
 91 
     | 
    
         
            +
                end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
       65 
93 
     | 
    
         
             
                def redirect_to_login
         
     | 
| 
       66 
94 
     | 
    
         
             
                  if request.xhr?
         
     | 
| 
       67 
     | 
    
         
            -
                    head 
     | 
| 
      
 95 
     | 
    
         
            +
                    head(:unauthorized)
         
     | 
| 
       68 
96 
     | 
    
         
             
                  else
         
     | 
| 
       69 
97 
     | 
    
         
             
                    if request.get?
         
     | 
| 
       70 
98 
     | 
    
         
             
                      path = request.path
         
     | 
| 
         @@ -74,7 +102,7 @@ module ShopifyApp 
     | 
|
| 
       74 
102 
     | 
    
         
             
                      path = referer.path
         
     | 
| 
       75 
103 
     | 
    
         
             
                      query = "#{referer.query}&#{sanitized_params.to_query}"
         
     | 
| 
       76 
104 
     | 
    
         
             
                    end
         
     | 
| 
       77 
     | 
    
         
            -
                    session[:return_to] = "#{path}?#{query}"
         
     | 
| 
      
 105 
     | 
    
         
            +
                    session[:return_to] = query.blank? ? path.to_s : "#{path}?#{query}"
         
     | 
| 
       78 
106 
     | 
    
         
             
                    redirect_to(login_url_with_optional_shop)
         
     | 
| 
       79 
107 
     | 
    
         
             
                  end
         
     | 
| 
       80 
108 
     | 
    
         
             
                end
         
     | 
| 
         @@ -105,7 +133,7 @@ module ShopifyApp 
     | 
|
| 
       105 
133 
     | 
    
         
             
                  query_params = {}
         
     | 
| 
       106 
134 
     | 
    
         
             
                  query_params[:shop] = sanitized_params[:shop] if params[:shop].present?
         
     | 
| 
       107 
135 
     | 
    
         | 
| 
       108 
     | 
    
         
            -
                  return_to = session[:return_to] || params[:return_to]
         
     | 
| 
      
 136 
     | 
    
         
            +
                  return_to = RedirectSafely.make_safe(session[:return_to] || params[:return_to], nil)
         
     | 
| 
       109 
137 
     | 
    
         | 
| 
       110 
138 
     | 
    
         
             
                  if return_to.present? && return_to_param_required?
         
     | 
| 
       111 
139 
     | 
    
         
             
                    query_params[:return_to] = return_to
         
     | 
| 
         @@ -128,14 +156,18 @@ module ShopifyApp 
     | 
|
| 
       128 
156 
     | 
    
         | 
| 
       129 
157 
     | 
    
         
             
                def fullpage_redirect_to(url)
         
     | 
| 
       130 
158 
     | 
    
         
             
                  if ShopifyApp.configuration.embedded_app?
         
     | 
| 
       131 
     | 
    
         
            -
                    render 
     | 
| 
      
 159 
     | 
    
         
            +
                    render('shopify_app/shared/redirect', layout: false,
         
     | 
| 
      
 160 
     | 
    
         
            +
                           locals: { url: url, current_shopify_domain: current_shopify_domain })
         
     | 
| 
       132 
161 
     | 
    
         
             
                  else
         
     | 
| 
       133 
     | 
    
         
            -
                    redirect_to 
     | 
| 
      
 162 
     | 
    
         
            +
                    redirect_to(url)
         
     | 
| 
       134 
163 
     | 
    
         
             
                  end
         
     | 
| 
       135 
164 
     | 
    
         
             
                end
         
     | 
| 
       136 
165 
     | 
    
         | 
| 
       137 
166 
     | 
    
         
             
                def current_shopify_domain
         
     | 
| 
       138 
     | 
    
         
            -
                  shopify_domain = sanitized_shop_name || 
     | 
| 
      
 167 
     | 
    
         
            +
                  shopify_domain = sanitized_shop_name ||
         
     | 
| 
      
 168 
     | 
    
         
            +
                    jwt_shopify_domain ||
         
     | 
| 
      
 169 
     | 
    
         
            +
                    session[:shopify_domain]
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
       139 
171 
     | 
    
         
             
                  return shopify_domain if shopify_domain.present?
         
     | 
| 
       140 
172 
     | 
    
         | 
| 
       141 
173 
     | 
    
         
             
                  raise ShopifyDomainNotFound
         
     | 
| 
         @@ -170,11 +202,16 @@ module ShopifyApp 
     | 
|
| 
       170 
202 
     | 
    
         
             
                end
         
     | 
| 
       171 
203 
     | 
    
         | 
| 
       172 
204 
     | 
    
         
             
                def return_address
         
     | 
| 
      
 205 
     | 
    
         
            +
                  return base_return_address unless ShopifyApp.configuration.allow_jwt_authentication
         
     | 
| 
      
 206 
     | 
    
         
            +
                  return_address_with_params(shop: current_shopify_domain)
         
     | 
| 
      
 207 
     | 
    
         
            +
                end
         
     | 
| 
      
 208 
     | 
    
         
            +
             
     | 
| 
      
 209 
     | 
    
         
            +
                def base_return_address
         
     | 
| 
       173 
210 
     | 
    
         
             
                  session.delete(:return_to) || ShopifyApp.configuration.root_url
         
     | 
| 
       174 
211 
     | 
    
         
             
                end
         
     | 
| 
       175 
212 
     | 
    
         | 
| 
       176 
213 
     | 
    
         
             
                def return_address_with_params(params)
         
     | 
| 
       177 
     | 
    
         
            -
                  uri = URI( 
     | 
| 
      
 214 
     | 
    
         
            +
                  uri = URI(base_return_address)
         
     | 
| 
       178 
215 
     | 
    
         
             
                  uri.query = CGI.parse(uri.query.to_s)
         
     | 
| 
       179 
216 
     | 
    
         
             
                    .symbolize_keys
         
     | 
| 
       180 
217 
     | 
    
         
             
                    .transform_values { |v| v.one? ? v.first : v }
         
     | 
| 
         @@ -0,0 +1,24 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
            module ShopifyApp
         
     | 
| 
      
 3 
     | 
    
         
            +
              module PayloadVerification
         
     | 
| 
      
 4 
     | 
    
         
            +
                extend ActiveSupport::Concern
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                private
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def shopify_hmac
         
     | 
| 
      
 9 
     | 
    
         
            +
                  request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def hmac_valid?(data)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  secrets = [ShopifyApp.configuration.secret, ShopifyApp.configuration.old_secret].reject(&:blank?)
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  secrets.any? do |secret|
         
     | 
| 
      
 16 
     | 
    
         
            +
                    digest = OpenSSL::Digest.new('sha256')
         
     | 
| 
      
 17 
     | 
    
         
            +
                    ActiveSupport::SecurityUtils.secure_compare(
         
     | 
| 
      
 18 
     | 
    
         
            +
                      shopify_hmac,
         
     | 
| 
      
 19 
     | 
    
         
            +
                      Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, data))
         
     | 
| 
      
 20 
     | 
    
         
            +
                    )
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -1,6 +1,8 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
       1 
2 
     | 
    
         
             
            module ShopifyApp
         
     | 
| 
       2 
3 
     | 
    
         
             
              module WebhookVerification
         
     | 
| 
       3 
4 
     | 
    
         
             
                extend ActiveSupport::Concern
         
     | 
| 
      
 5 
     | 
    
         
            +
                include ShopifyApp::PayloadVerification
         
     | 
| 
       4 
6 
     | 
    
         | 
| 
       5 
7 
     | 
    
         
             
                included do
         
     | 
| 
       6 
8 
     | 
    
         
             
                  skip_before_action :verify_authenticity_token, raise: false
         
     | 
| 
         @@ -11,28 +13,11 @@ module ShopifyApp 
     | 
|
| 
       11 
13 
     | 
    
         | 
| 
       12 
14 
     | 
    
         
             
                def verify_request
         
     | 
| 
       13 
15 
     | 
    
         
             
                  data = request.raw_post
         
     | 
| 
       14 
     | 
    
         
            -
                  return head 
     | 
| 
       15 
     | 
    
         
            -
                end
         
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
                def hmac_valid?(data)
         
     | 
| 
       18 
     | 
    
         
            -
                  secrets = [ShopifyApp.configuration.secret, ShopifyApp.configuration.old_secret].reject(&:blank?)
         
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
                  secrets.any? do |secret|
         
     | 
| 
       21 
     | 
    
         
            -
                    digest = OpenSSL::Digest.new('sha256')
         
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
     | 
    
         
            -
                    ActiveSupport::SecurityUtils.secure_compare(
         
     | 
| 
       24 
     | 
    
         
            -
                      shopify_hmac,
         
     | 
| 
       25 
     | 
    
         
            -
                      Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, data))
         
     | 
| 
       26 
     | 
    
         
            -
                    )
         
     | 
| 
       27 
     | 
    
         
            -
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
                  return head(:unauthorized) unless hmac_valid?(data)
         
     | 
| 
       28 
17 
     | 
    
         
             
                end
         
     | 
| 
       29 
18 
     | 
    
         | 
| 
       30 
19 
     | 
    
         
             
                def shop_domain
         
     | 
| 
       31 
20 
     | 
    
         
             
                  request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN']
         
     | 
| 
       32 
21 
     | 
    
         
             
                end
         
     | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
                def shopify_hmac
         
     | 
| 
       35 
     | 
    
         
            -
                  request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']
         
     | 
| 
       36 
     | 
    
         
            -
                end
         
     | 
| 
       37 
22 
     | 
    
         
             
              end
         
     | 
| 
       38 
23 
     | 
    
         
             
            end
         
     | 
    
        data/lib/shopify_app/engine.rb
    CHANGED
    
    | 
         @@ -1,3 +1,4 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
       1 
2 
     | 
    
         
             
            module ShopifyApp
         
     | 
| 
       2 
3 
     | 
    
         
             
              class Engine < Rails::Engine
         
     | 
| 
       3 
4 
     | 
    
         
             
                engine_name 'shopify_app'
         
     | 
| 
         @@ -15,6 +16,10 @@ module ShopifyApp 
     | 
|
| 
       15 
16 
     | 
    
         | 
| 
       16 
17 
     | 
    
         
             
                initializer "shopify_app.middleware" do |app|
         
     | 
| 
       17 
18 
     | 
    
         
             
                  app.config.middleware.insert_after(::Rack::Runtime, ShopifyApp::SameSiteCookieMiddleware)
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  if ShopifyApp.configuration.allow_jwt_authentication
         
     | 
| 
      
 21 
     | 
    
         
            +
                    app.config.middleware.insert_after(ShopifyApp::SameSiteCookieMiddleware, ShopifyApp::JWTMiddleware)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
       18 
23 
     | 
    
         
             
                end
         
     | 
| 
       19 
24 
     | 
    
         
             
              end
         
     | 
| 
       20 
25 
     | 
    
         
             
            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  
     | 
| 
      
 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  
     | 
| 
       59 
     | 
    
         
            -
                  scripttags.map{ |w| w[:src] }.include? 
     | 
| 
      
 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  
     | 
| 
      
 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  
     | 
| 
       43 
     | 
    
         
            -
                  required_webhooks.map{ |w| w[:address] }.include? 
     | 
| 
      
 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)
         
     | 
| 
         @@ -0,0 +1,42 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
            module ShopifyApp
         
     | 
| 
      
 3 
     | 
    
         
            +
              class JWTMiddleware
         
     | 
| 
      
 4 
     | 
    
         
            +
                TOKEN_REGEX = /^Bearer\s+(.*?)$/
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                def initialize(app)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @app = app
         
     | 
| 
      
 8 
     | 
    
         
            +
                end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def call(env)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  return call_next(env) unless authorization_header(env)
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  token = extract_token(env)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  return call_next(env) unless token
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  set_env_variables(token, env)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  call_next(env)
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                private
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                def call_next(env)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @app.call(env)
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def authorization_header(env)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  env['HTTP_AUTHORIZATION']
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                def extract_token(env)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  match = authorization_header(env).match(TOKEN_REGEX)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  match && match[1]
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def set_env_variables(token, env)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  jwt = ShopifyApp::JWT.new(token)
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  env['jwt.shopify_domain'] = jwt.shopify_domain
         
     | 
| 
      
 39 
     | 
    
         
            +
                  env['jwt.shopify_user_id'] = jwt.shopify_user_id
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
              end
         
     | 
| 
      
 42 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -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'  
     | 
| 
      
 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, * 
     | 
| 
      
 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 
     | 
| 
       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
         
     |