shopify_app 13.5.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 +7 -0
- data/.babelrc +5 -0
- data/.github/CODEOWNERS +1 -0
- data/.github/ISSUE_TEMPLATE.md +14 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +6 -0
- data/.github/probots.yml +2 -0
- data/.github/workflows/rubocop.yml +28 -0
- data/.gitignore +16 -0
- data/.nvmrc +1 -0
- data/.rubocop.yml +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +28 -0
- data/CHANGELOG.md +505 -0
- data/Gemfile +11 -0
- data/LICENSE +19 -0
- data/README.md +620 -0
- data/Rakefile +7 -0
- data/SECURITY.md +59 -0
- data/app/assets/images/storage_access.svg +2 -0
- data/app/assets/javascripts/shopify_app/enable_cookies.js +3 -0
- data/app/assets/javascripts/shopify_app/itp_helper.js +40 -0
- data/app/assets/javascripts/shopify_app/partition_cookies.js +8 -0
- data/app/assets/javascripts/shopify_app/redirect.js +33 -0
- data/app/assets/javascripts/shopify_app/request_storage_access.js +3 -0
- data/app/assets/javascripts/shopify_app/storage_access.js +153 -0
- data/app/assets/javascripts/shopify_app/storage_access_redirect.js +17 -0
- data/app/assets/javascripts/shopify_app/top_level.js +2 -0
- data/app/assets/javascripts/shopify_app/top_level_interaction.js +11 -0
- data/app/controllers/concerns/shopify_app/authenticated.rb +16 -0
- data/app/controllers/concerns/shopify_app/require_known_shop.rb +39 -0
- data/app/controllers/shopify_app/authenticated_controller.rb +8 -0
- data/app/controllers/shopify_app/callback_controller.rb +140 -0
- data/app/controllers/shopify_app/extension_verification_controller.rb +15 -0
- data/app/controllers/shopify_app/sessions_controller.rb +184 -0
- data/app/controllers/shopify_app/webhooks_controller.rb +37 -0
- data/app/views/shopify_app/partials/_button_styles.html.erb +104 -0
- data/app/views/shopify_app/partials/_card_styles.html.erb +33 -0
- data/app/views/shopify_app/partials/_empty_state_styles.html.erb +129 -0
- data/app/views/shopify_app/partials/_layout_styles.html.erb +167 -0
- data/app/views/shopify_app/partials/_typography_styles.html.erb +35 -0
- data/app/views/shopify_app/sessions/enable_cookies.html.erb +75 -0
- data/app/views/shopify_app/sessions/new.html.erb +123 -0
- data/app/views/shopify_app/sessions/request_storage_access.html.erb +68 -0
- data/app/views/shopify_app/sessions/top_level_interaction.html.erb +64 -0
- data/app/views/shopify_app/shared/redirect.html.erb +23 -0
- data/config/locales/cs.yml +23 -0
- data/config/locales/da.yml +20 -0
- data/config/locales/de.yml +22 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/es.yml +22 -0
- data/config/locales/fi.yml +20 -0
- data/config/locales/fr.yml +23 -0
- data/config/locales/hi.yml +23 -0
- data/config/locales/it.yml +21 -0
- data/config/locales/ja.yml +17 -0
- data/config/locales/ko.yml +19 -0
- data/config/locales/ms.yml +22 -0
- data/config/locales/nb.yml +21 -0
- data/config/locales/nl.yml +21 -0
- data/config/locales/pl.yml +21 -0
- data/config/locales/pt-BR.yml +21 -0
- data/config/locales/pt-PT.yml +22 -0
- data/config/locales/sv.yml +21 -0
- data/config/locales/th.yml +20 -0
- data/config/locales/tr.yml +22 -0
- data/config/locales/zh-CN.yml +16 -0
- data/config/locales/zh-TW.yml +16 -0
- data/config/routes.rb +23 -0
- data/docs/Quickstart.md +93 -0
- data/docs/Releasing.md +18 -0
- data/docs/Troubleshooting.md +16 -0
- data/docs/install-on-dev-shop.png +0 -0
- data/docs/test-your-app.png +0 -0
- data/images/app-proxy-screenshot.png +0 -0
- data/karma.conf.js +44 -0
- data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +47 -0
- data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +11 -0
- data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +40 -0
- data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +62 -0
- data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +69 -0
- data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +13 -0
- data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +26 -0
- data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +8 -0
- data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +11 -0
- data/lib/generators/shopify_app/app_proxy_controller/templates/index.html.erb +19 -0
- data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +15 -0
- data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +5 -0
- data/lib/generators/shopify_app/controllers/controllers_generator.rb +30 -0
- data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +26 -0
- data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +8 -0
- data/lib/generators/shopify_app/home_controller/templates/index.html.erb +21 -0
- data/lib/generators/shopify_app/install/install_generator.rb +83 -0
- data/lib/generators/shopify_app/install/templates/_flash_messages.html.erb +3 -0
- data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +41 -0
- data/lib/generators/shopify_app/install/templates/flash_messages.js +24 -0
- data/lib/generators/shopify_app/install/templates/omniauth.rb +3 -0
- data/lib/generators/shopify_app/install/templates/session_store.rb +4 -0
- data/lib/generators/shopify_app/install/templates/shopify_app.js +15 -0
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +15 -0
- data/lib/generators/shopify_app/install/templates/shopify_app_index.js +2 -0
- data/lib/generators/shopify_app/install/templates/shopify_provider.rb +20 -0
- data/lib/generators/shopify_app/install/templates/user_agent.rb +6 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +16 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +17 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +42 -0
- data/lib/generators/shopify_app/routes/routes_generator.rb +32 -0
- data/lib/generators/shopify_app/routes/templates/routes.rb +12 -0
- data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +43 -0
- data/lib/generators/shopify_app/shop_model/templates/db/migrate/create_shops.erb +15 -0
- data/lib/generators/shopify_app/shop_model/templates/shop.rb +8 -0
- data/lib/generators/shopify_app/shop_model/templates/shops.yml +3 -0
- data/lib/generators/shopify_app/shopify_app_generator.rb +18 -0
- data/lib/generators/shopify_app/user_model/templates/db/migrate/create_users.erb +16 -0
- data/lib/generators/shopify_app/user_model/templates/user.rb +8 -0
- data/lib/generators/shopify_app/user_model/templates/users.yml +4 -0
- data/lib/generators/shopify_app/user_model/user_model_generator.rb +43 -0
- data/lib/generators/shopify_app/views/views_generator.rb +30 -0
- data/lib/shopify_app.rb +61 -0
- data/lib/shopify_app/configuration.rb +94 -0
- data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +38 -0
- data/lib/shopify_app/controller_concerns/csrf_protection.rb +15 -0
- data/lib/shopify_app/controller_concerns/embedded_app.rb +20 -0
- data/lib/shopify_app/controller_concerns/itp.rb +45 -0
- data/lib/shopify_app/controller_concerns/localization.rb +23 -0
- data/lib/shopify_app/controller_concerns/login_protection.rb +231 -0
- data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
- data/lib/shopify_app/controller_concerns/webhook_verification.rb +23 -0
- data/lib/shopify_app/engine.rb +25 -0
- data/lib/shopify_app/jobs/scripttags_manager_job.rb +16 -0
- data/lib/shopify_app/jobs/webhooks_manager_job.rb +16 -0
- data/lib/shopify_app/managers/scripttags_manager.rb +78 -0
- data/lib/shopify_app/managers/webhooks_manager.rb +62 -0
- data/lib/shopify_app/middleware/jwt_middleware.rb +42 -0
- data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +34 -0
- data/lib/shopify_app/session/in_memory_session_store.rb +31 -0
- data/lib/shopify_app/session/in_memory_shop_session_store.rb +14 -0
- data/lib/shopify_app/session/in_memory_user_session_store.rb +14 -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 +56 -0
- data/lib/shopify_app/session/session_storage.rb +20 -0
- data/lib/shopify_app/session/shop_session_storage.rb +42 -0
- data/lib/shopify_app/session/user_session_storage.rb +42 -0
- 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 +24 -0
- data/lib/shopify_app/version.rb +4 -0
- data/package-lock.json +7177 -0
- data/package.json +28 -0
- data/service.yml +7 -0
- data/shipit.rubygems.yml +4 -0
- data/shopify_app.gemspec +37 -0
- data/translation.yml +7 -0
- data/webpack.config.js +24 -0
- data/yarn.lock +5263 -0
- metadata +420 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
module AppProxyVerification
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
included do
|
|
6
|
+
skip_before_action :verify_authenticity_token, raise: false
|
|
7
|
+
before_action :verify_proxy_request
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def verify_proxy_request
|
|
11
|
+
return head(:forbidden) unless query_string_valid?(request.query_string)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def query_string_valid?(query_string)
|
|
17
|
+
query_hash = Rack::Utils.parse_query(query_string)
|
|
18
|
+
|
|
19
|
+
signature = query_hash.delete('signature')
|
|
20
|
+
return false if signature.nil?
|
|
21
|
+
|
|
22
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
|
23
|
+
calculated_signature(query_hash),
|
|
24
|
+
signature
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def calculated_signature(query_hash_without_signature)
|
|
29
|
+
sorted_params = query_hash_without_signature.collect { |k, v| "#{k}=#{Array(v).join(',')}" }.sort.join
|
|
30
|
+
|
|
31
|
+
OpenSSL::HMAC.hexdigest(
|
|
32
|
+
OpenSSL::Digest.new('sha256'),
|
|
33
|
+
ShopifyApp.configuration.secret,
|
|
34
|
+
sorted_params
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
module EmbeddedApp
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
if ShopifyApp.configuration.embedded_app?
|
|
8
|
+
after_action(:set_esdk_headers)
|
|
9
|
+
layout('embedded_app')
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def set_esdk_headers
|
|
16
|
+
response.set_header('P3P', 'CP="Not used"')
|
|
17
|
+
response.headers.except!('X-Frame-Options')
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
# Cookie management helpers required for ITP implementation
|
|
5
|
+
module Itp
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def set_test_cookie
|
|
9
|
+
return unless ShopifyApp.configuration.embedded_app?
|
|
10
|
+
return unless user_agent_can_partition_cookies
|
|
11
|
+
|
|
12
|
+
session['shopify.cookies_persist'] = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def set_top_level_oauth_cookie
|
|
16
|
+
session['shopify.top_level_oauth'] = true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def clear_top_level_oauth_cookie
|
|
20
|
+
session.delete('shopify.top_level_oauth')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def user_agent_is_mobile
|
|
24
|
+
user_agent = BrowserSniffer.new(request.user_agent).browser_info
|
|
25
|
+
|
|
26
|
+
user_agent[:name].to_s.match(/Shopify\sMobile/)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def user_agent_is_pos
|
|
30
|
+
user_agent = BrowserSniffer.new(request.user_agent).browser_info
|
|
31
|
+
|
|
32
|
+
user_agent[:name].to_s.match(/Shopify\sPOS/)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def user_agent_can_partition_cookies
|
|
36
|
+
user_agent = BrowserSniffer.new(request.user_agent).browser_info
|
|
37
|
+
|
|
38
|
+
is_safari = user_agent[:name].to_s.match(/Safari/)
|
|
39
|
+
|
|
40
|
+
return false unless is_safari
|
|
41
|
+
|
|
42
|
+
user_agent[:version].to_s.match(/12\.0/)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
module Localization
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
before_action :set_locale
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def set_locale
|
|
13
|
+
if params[:locale]
|
|
14
|
+
session[:locale] = params[:locale]
|
|
15
|
+
else
|
|
16
|
+
session[:locale] ||= I18n.default_locale
|
|
17
|
+
end
|
|
18
|
+
I18n.locale = session[:locale]
|
|
19
|
+
rescue I18n::InvalidLocale
|
|
20
|
+
I18n.locale = I18n.default_locale
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'browser_sniffer'
|
|
4
|
+
|
|
5
|
+
module ShopifyApp
|
|
6
|
+
module LoginProtection
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
include ShopifyApp::Itp
|
|
9
|
+
|
|
10
|
+
class ShopifyDomainNotFound < StandardError; end
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
after_action :set_test_cookie
|
|
14
|
+
rescue_from ActiveResource::UnauthorizedAccess, with: :close_session
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
ACCESS_TOKEN_REQUIRED_HEADER = 'X-Shopify-API-Request-Failure-Unauthorized'
|
|
18
|
+
|
|
19
|
+
def activate_shopify_session
|
|
20
|
+
return redirect_to_login if current_shopify_session.blank?
|
|
21
|
+
clear_top_level_oauth_cookie
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
ShopifyAPI::Base.activate_session(current_shopify_session)
|
|
25
|
+
yield
|
|
26
|
+
ensure
|
|
27
|
+
ShopifyAPI::Base.clear_session
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def current_shopify_session
|
|
32
|
+
@current_shopify_session ||= begin
|
|
33
|
+
user_session || shop_session
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def user_session
|
|
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?
|
|
49
|
+
ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def shop_session
|
|
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?
|
|
64
|
+
ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def login_again_if_different_user_or_shop
|
|
68
|
+
if session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
|
|
69
|
+
clear_session = session[:user_session] != params[:session] # current user is different from stored user
|
|
70
|
+
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if current_shopify_session &&
|
|
74
|
+
params[:shop] && params[:shop].is_a?(String) &&
|
|
75
|
+
(current_shopify_session.domain != params[:shop])
|
|
76
|
+
clear_session = true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if clear_session
|
|
80
|
+
clear_shopify_session
|
|
81
|
+
redirect_to_login
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def signal_access_token_required
|
|
86
|
+
response.set_header(ACCESS_TOKEN_REQUIRED_HEADER, true)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
protected
|
|
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
|
+
|
|
99
|
+
def redirect_to_login
|
|
100
|
+
if request.xhr?
|
|
101
|
+
head(:unauthorized)
|
|
102
|
+
else
|
|
103
|
+
if request.get?
|
|
104
|
+
path = request.path
|
|
105
|
+
query = sanitized_params.to_query
|
|
106
|
+
else
|
|
107
|
+
referer = URI(request.referer || "/")
|
|
108
|
+
path = referer.path
|
|
109
|
+
query = "#{referer.query}&#{sanitized_params.to_query}"
|
|
110
|
+
end
|
|
111
|
+
session[:return_to] = query.blank? ? path.to_s : "#{path}?#{query}"
|
|
112
|
+
redirect_to(login_url_with_optional_shop)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def close_session
|
|
117
|
+
clear_shopify_session
|
|
118
|
+
redirect_to(login_url_with_optional_shop)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def clear_shopify_session
|
|
122
|
+
session[:shop_id] = nil
|
|
123
|
+
session[:user_id] = nil
|
|
124
|
+
session[:shopify_domain] = nil
|
|
125
|
+
session[:shopify_user] = nil
|
|
126
|
+
session[:user_session] = nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def login_url_with_optional_shop(top_level: false)
|
|
130
|
+
url = ShopifyApp.configuration.login_url
|
|
131
|
+
|
|
132
|
+
query_params = login_url_params(top_level: top_level)
|
|
133
|
+
|
|
134
|
+
url = "#{url}?#{query_params.to_query}" if query_params.present?
|
|
135
|
+
url
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def login_url_params(top_level:)
|
|
139
|
+
query_params = {}
|
|
140
|
+
query_params[:shop] = sanitized_params[:shop] if params[:shop].present?
|
|
141
|
+
|
|
142
|
+
return_to = RedirectSafely.make_safe(session[:return_to] || params[:return_to], nil)
|
|
143
|
+
|
|
144
|
+
if return_to.present? && return_to_param_required?
|
|
145
|
+
query_params[:return_to] = return_to
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
has_referer_shop_name = referer_sanitized_shop_name.present?
|
|
149
|
+
|
|
150
|
+
if has_referer_shop_name
|
|
151
|
+
query_params[:shop] ||= referer_sanitized_shop_name
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
query_params[:top_level] = true if top_level
|
|
155
|
+
query_params
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def return_to_param_required?
|
|
159
|
+
native_params = %i[shop hmac timestamp locale protocol return_to]
|
|
160
|
+
request.path != '/' || sanitized_params.except(*native_params).any?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def fullpage_redirect_to(url)
|
|
164
|
+
if ShopifyApp.configuration.embedded_app?
|
|
165
|
+
render('shopify_app/shared/redirect', layout: false,
|
|
166
|
+
locals: { url: url, current_shopify_domain: current_shopify_domain })
|
|
167
|
+
else
|
|
168
|
+
redirect_to(url)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def current_shopify_domain
|
|
173
|
+
shopify_domain = sanitized_shop_name ||
|
|
174
|
+
jwt_shopify_domain ||
|
|
175
|
+
session[:shopify_domain]
|
|
176
|
+
|
|
177
|
+
return shopify_domain if shopify_domain.present?
|
|
178
|
+
|
|
179
|
+
raise ShopifyDomainNotFound
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def sanitized_shop_name
|
|
183
|
+
@sanitized_shop_name ||= sanitize_shop_param(params)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def referer_sanitized_shop_name
|
|
187
|
+
return unless request.referer.present?
|
|
188
|
+
|
|
189
|
+
@referer_sanitized_shop_name ||= begin
|
|
190
|
+
referer_uri = URI(request.referer)
|
|
191
|
+
query_params = Rack::Utils.parse_query(referer_uri.query)
|
|
192
|
+
|
|
193
|
+
sanitize_shop_param(query_params.with_indifferent_access)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def sanitize_shop_param(params)
|
|
198
|
+
return unless params[:shop].present?
|
|
199
|
+
ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def sanitized_params
|
|
203
|
+
request.query_parameters.clone.tap do |query_params|
|
|
204
|
+
if params[:shop].is_a?(String)
|
|
205
|
+
query_params[:shop] = sanitize_shop_param(params)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
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
|
|
218
|
+
session.delete(:return_to) || ShopifyApp.configuration.root_url
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def return_address_with_params(params)
|
|
222
|
+
uri = URI(base_return_address)
|
|
223
|
+
uri.query = CGI.parse(uri.query.to_s)
|
|
224
|
+
.symbolize_keys
|
|
225
|
+
.transform_values { |v| v.one? ? v.first : v }
|
|
226
|
+
.merge(params)
|
|
227
|
+
.to_query
|
|
228
|
+
uri.to_s
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
module WebhookVerification
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
include ShopifyApp::PayloadVerification
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
skip_before_action :verify_authenticity_token, raise: false
|
|
9
|
+
before_action :verify_request
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def verify_request
|
|
15
|
+
data = request.raw_post
|
|
16
|
+
return head(:unauthorized) unless hmac_valid?(data)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def shop_domain
|
|
20
|
+
request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN']
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class Engine < Rails::Engine
|
|
4
|
+
engine_name 'shopify_app'
|
|
5
|
+
isolate_namespace ShopifyApp
|
|
6
|
+
|
|
7
|
+
initializer "shopify_app.assets.precompile" do |app|
|
|
8
|
+
app.config.assets.precompile += %w[
|
|
9
|
+
shopify_app/redirect.js
|
|
10
|
+
shopify_app/top_level.js
|
|
11
|
+
shopify_app/enable_cookies.js
|
|
12
|
+
shopify_app/request_storage_access.js
|
|
13
|
+
storage_access.svg
|
|
14
|
+
]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer "shopify_app.middleware" do |app|
|
|
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
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|