ruby_shopify_app 1.0.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 +2 -0
- data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
- data/.github/ISSUE_TEMPLATE/config.yml +1 -0
- data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- data/.github/probots.yml +2 -0
- data/.github/workflows/build.yml +40 -0
- data/.github/workflows/release.yml +24 -0
- data/.github/workflows/rubocop.yml +22 -0
- data/.gitignore +14 -0
- data/.nvmrc +1 -0
- data/.rubocop.yml +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG-OLD.md +643 -0
- data/CHANGELOG.md +6 -0
- data/CONTRIBUTING.md +81 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +280 -0
- data/LICENSE +19 -0
- data/README.md +132 -0
- data/Rakefile +7 -0
- data/SECURITY.md +59 -0
- data/app/assets/images/storage_access.svg +1 -0
- data/app/assets/javascripts/shopify_app/app_bridge_2.0.12.js +10 -0
- data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +22 -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/post_redirect.js +9 -0
- data/app/assets/javascripts/shopify_app/redirect.js +31 -0
- data/app/assets/javascripts/shopify_app/request_storage_access.js +3 -0
- data/app/assets/javascripts/shopify_app/storage_access.js +148 -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/ensure_authenticated_links.rb +39 -0
- data/app/controllers/concerns/shopify_app/require_known_shop.rb +40 -0
- data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
- data/app/controllers/shopify_app/authenticated_controller.rb +8 -0
- data/app/controllers/shopify_app/callback_controller.rb +195 -0
- data/app/controllers/shopify_app/extension_verification_controller.rb +15 -0
- data/app/controllers/shopify_app/sessions_controller.rb +202 -0
- data/app/controllers/shopify_app/webhooks_controller.rb +36 -0
- data/app/views/shopify_app/partials/_button_styles.html.erb +109 -0
- data/app/views/shopify_app/partials/_card_styles.html.erb +33 -0
- data/app/views/shopify_app/partials/_empty_state_styles.html.erb +98 -0
- data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
- data/app/views/shopify_app/partials/_layout_styles.html.erb +182 -0
- data/app/views/shopify_app/partials/_typography_styles.html.erb +35 -0
- data/app/views/shopify_app/sessions/enable_cookies.html.erb +70 -0
- data/app/views/shopify_app/sessions/new.html.erb +51 -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 +63 -0
- data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -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/it.yml +21 -0
- data/config/locales/ja.yml +17 -0
- data/config/locales/ko.yml +19 -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/vi.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 +31 -0
- data/docs/Releasing.md +21 -0
- data/docs/Troubleshooting.md +159 -0
- data/docs/Upgrading.md +132 -0
- data/docs/shopify_app/authentication.md +124 -0
- data/docs/shopify_app/engine.md +82 -0
- data/docs/shopify_app/generators.md +127 -0
- data/docs/shopify_app/handling-access-scopes-changes.md +24 -0
- data/docs/shopify_app/script-tags.md +28 -0
- data/docs/shopify_app/session-repository.md +88 -0
- data/docs/shopify_app/testing.md +38 -0
- data/docs/shopify_app/webhooks.md +72 -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 +53 -0
- data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +18 -0
- data/lib/generators/shopify_app/home_controller/templates/index.html.erb +75 -0
- data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +12 -0
- data/lib/generators/shopify_app/install/install_generator.rb +121 -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 +44 -0
- data/lib/generators/shopify_app/install/templates/flash_messages.js +24 -0
- data/lib/generators/shopify_app/install/templates/omniauth.rb +4 -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 +25 -0
- data/lib/generators/shopify_app/install/templates/shopify_app_importmap.js +13 -0
- data/lib/generators/shopify_app/install/templates/shopify_app_index.js +2 -0
- data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +8 -0
- data/lib/generators/shopify_app/install/templates/user_agent.rb +6 -0
- data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +19 -0
- data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +8 -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 +70 -0
- data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -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/add_user_access_scopes_column.erb +5 -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 +70 -0
- data/lib/generators/shopify_app/views/views_generator.rb +30 -0
- data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
- data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
- data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
- data/lib/shopify_app/configuration.rb +119 -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 +259 -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 +47 -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 +43 -0
- data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +34 -0
- data/lib/shopify_app/omniauth/omniauth_configuration.rb +64 -0
- data/lib/shopify_app/session/in_memory_session_store.rb +31 -0
- data/lib/shopify_app/session/in_memory_shop_session_store.rb +16 -0
- data/lib/shopify_app/session/in_memory_user_session_store.rb +16 -0
- data/lib/shopify_app/session/jwt.rb +67 -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/shop_session_storage_with_scopes.rb +58 -0
- data/lib/shopify_app/session/user_session_storage.rb +42 -0
- data/lib/shopify_app/session/user_session_storage_with_scopes.rb +58 -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 +37 -0
- data/lib/shopify_app/version.rb +4 -0
- data/lib/shopify_app.rb +80 -0
- data/package.json +27 -0
- data/service.yml +4 -0
- data/shipit.rubygems.yml +4 -0
- data/shopify_app.gemspec +39 -0
- data/translation.yml +7 -0
- data/webpack.config.js +24 -0
- data/yarn.lock +5230 -0
- metadata +465 -0
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
class ShopifyHostNotFound < StandardError; end
|
|
13
|
+
|
|
14
|
+
included do
|
|
15
|
+
after_action :set_test_cookie
|
|
16
|
+
rescue_from ActiveResource::UnauthorizedAccess, with: :close_session
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
ACCESS_TOKEN_REQUIRED_HEADER = 'X-Shopify-API-Request-Failure-Unauthorized'
|
|
20
|
+
|
|
21
|
+
def activate_shopify_session
|
|
22
|
+
if user_session_expected? && user_session.blank?
|
|
23
|
+
signal_access_token_required
|
|
24
|
+
return redirect_to_login
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return redirect_to_login if current_shopify_session.blank?
|
|
28
|
+
|
|
29
|
+
clear_top_level_oauth_cookie
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
ShopifyAPI::Base.activate_session(current_shopify_session)
|
|
33
|
+
yield
|
|
34
|
+
ensure
|
|
35
|
+
ShopifyAPI::Base.clear_session
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def current_shopify_session
|
|
40
|
+
@current_shopify_session ||= begin
|
|
41
|
+
user_session || shop_session
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def user_session
|
|
46
|
+
user_session_by_jwt || user_session_by_cookie
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def user_session_by_jwt
|
|
50
|
+
return unless ShopifyApp.configuration.allow_jwt_authentication
|
|
51
|
+
return unless jwt_shopify_user_id
|
|
52
|
+
ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(jwt_shopify_user_id)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def user_session_by_cookie
|
|
56
|
+
return unless ShopifyApp.configuration.allow_cookie_authentication
|
|
57
|
+
return unless session[:user_id].present?
|
|
58
|
+
ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def shop_session
|
|
62
|
+
shop_session_by_jwt || shop_session_by_cookie
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def shop_session_by_jwt
|
|
66
|
+
return unless ShopifyApp.configuration.allow_jwt_authentication
|
|
67
|
+
return unless jwt_shopify_domain
|
|
68
|
+
ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(jwt_shopify_domain)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def shop_session_by_cookie
|
|
72
|
+
return unless ShopifyApp.configuration.allow_cookie_authentication
|
|
73
|
+
return unless session[:shop_id].present?
|
|
74
|
+
ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def login_again_if_different_user_or_shop
|
|
78
|
+
if session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
|
|
79
|
+
clear_session = session[:user_session] != params[:session] # current user is different from stored user
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if current_shopify_session &&
|
|
83
|
+
params[:shop] && params[:shop].is_a?(String) &&
|
|
84
|
+
(current_shopify_session.domain != params[:shop])
|
|
85
|
+
clear_session = true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if clear_session
|
|
89
|
+
clear_shopify_session
|
|
90
|
+
redirect_to_login
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def signal_access_token_required
|
|
95
|
+
response.set_header(ACCESS_TOKEN_REQUIRED_HEADER, "true")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def jwt_expire_at
|
|
99
|
+
expire_at = request.env['jwt.expire_at']
|
|
100
|
+
return unless expire_at
|
|
101
|
+
expire_at - 5.seconds # 5s gap to start fetching new token in advance
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
protected
|
|
105
|
+
|
|
106
|
+
def jwt_shopify_domain
|
|
107
|
+
request.env['jwt.shopify_domain']
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def jwt_shopify_user_id
|
|
111
|
+
request.env['jwt.shopify_user_id']
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def host
|
|
115
|
+
params[:host]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def redirect_to_login
|
|
119
|
+
if request.xhr?
|
|
120
|
+
head(:unauthorized)
|
|
121
|
+
else
|
|
122
|
+
if request.get?
|
|
123
|
+
path = request.path
|
|
124
|
+
query = sanitized_params.to_query
|
|
125
|
+
else
|
|
126
|
+
referer = URI(request.referer || "/")
|
|
127
|
+
path = referer.path
|
|
128
|
+
query = "#{referer.query}&#{sanitized_params.to_query}"
|
|
129
|
+
end
|
|
130
|
+
session[:return_to] = query.blank? ? path.to_s : "#{path}?#{query}"
|
|
131
|
+
redirect_to(login_url_with_optional_shop)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def close_session
|
|
136
|
+
clear_shopify_session
|
|
137
|
+
redirect_to(login_url_with_optional_shop)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def clear_shopify_session
|
|
141
|
+
session[:shop_id] = nil
|
|
142
|
+
session[:user_id] = nil
|
|
143
|
+
session[:shopify_domain] = nil
|
|
144
|
+
session[:shopify_user] = nil
|
|
145
|
+
session[:user_session] = nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def login_url_with_optional_shop(top_level: false)
|
|
149
|
+
url = ShopifyApp.configuration.login_url
|
|
150
|
+
|
|
151
|
+
query_params = login_url_params(top_level: top_level)
|
|
152
|
+
|
|
153
|
+
url = "#{url}?#{query_params.to_query}" if query_params.present?
|
|
154
|
+
url
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def login_url_params(top_level:)
|
|
158
|
+
query_params = {}
|
|
159
|
+
query_params[:shop] = sanitized_params[:shop] if params[:shop].present?
|
|
160
|
+
|
|
161
|
+
return_to = RedirectSafely.make_safe(session[:return_to] || params[:return_to], nil)
|
|
162
|
+
|
|
163
|
+
if return_to.present? && return_to_param_required?
|
|
164
|
+
query_params[:return_to] = return_to
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
has_referer_shop_name = referer_sanitized_shop_name.present?
|
|
168
|
+
|
|
169
|
+
if has_referer_shop_name
|
|
170
|
+
query_params[:shop] ||= referer_sanitized_shop_name
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if params[:host].present?
|
|
174
|
+
query_params[:host] ||= host
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
query_params[:top_level] = true if top_level
|
|
178
|
+
query_params
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def return_to_param_required?
|
|
182
|
+
native_params = %i[shop hmac timestamp locale protocol return_to]
|
|
183
|
+
request.path != '/' || sanitized_params.except(*native_params).any?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def fullpage_redirect_to(url)
|
|
187
|
+
if ShopifyApp.configuration.embedded_app?
|
|
188
|
+
render('shopify_app/shared/redirect', layout: false,
|
|
189
|
+
locals: { url: url, current_shopify_domain: current_shopify_domain })
|
|
190
|
+
else
|
|
191
|
+
redirect_to(url)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def current_shopify_domain
|
|
196
|
+
shopify_domain = sanitized_shop_name ||
|
|
197
|
+
jwt_shopify_domain ||
|
|
198
|
+
session[:shopify_domain]
|
|
199
|
+
|
|
200
|
+
return shopify_domain if shopify_domain.present?
|
|
201
|
+
|
|
202
|
+
raise ShopifyDomainNotFound
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def sanitized_shop_name
|
|
206
|
+
@sanitized_shop_name ||= sanitize_shop_param(params)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def referer_sanitized_shop_name
|
|
210
|
+
return unless request.referer.present?
|
|
211
|
+
|
|
212
|
+
@referer_sanitized_shop_name ||= begin
|
|
213
|
+
referer_uri = URI(request.referer)
|
|
214
|
+
query_params = Rack::Utils.parse_query(referer_uri.query)
|
|
215
|
+
|
|
216
|
+
sanitize_shop_param(query_params.with_indifferent_access)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def sanitize_shop_param(params)
|
|
221
|
+
return unless params[:shop].present?
|
|
222
|
+
ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def sanitized_params
|
|
226
|
+
request.query_parameters.clone.tap do |query_params|
|
|
227
|
+
if params[:shop].is_a?(String)
|
|
228
|
+
query_params[:shop] = sanitize_shop_param(params)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def return_address
|
|
234
|
+
return_address_with_params(shop: current_shopify_domain, host: host)
|
|
235
|
+
rescue ShopifyDomainNotFound, ShopifyHostNotFound
|
|
236
|
+
base_return_address
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def base_return_address
|
|
240
|
+
session.delete(:return_to) || ShopifyApp.configuration.root_url
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def return_address_with_params(params)
|
|
244
|
+
uri = URI(base_return_address)
|
|
245
|
+
uri.query = CGI.parse(uri.query.to_s)
|
|
246
|
+
.symbolize_keys
|
|
247
|
+
.transform_values { |v| v.one? ? v.first : v }
|
|
248
|
+
.merge(params)
|
|
249
|
+
.to_query
|
|
250
|
+
uri.to_s
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def user_session_expected?
|
|
256
|
+
!ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
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,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
module RedactJobParams
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
def args_info(job)
|
|
7
|
+
log_disabled_classes = %w(ShopifyApp::ScripttagsManagerJob ShopifyApp::WebhooksManagerJob)
|
|
8
|
+
return "" if log_disabled_classes.include?(job.class.name)
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Engine < Rails::Engine
|
|
14
|
+
engine_name 'shopify_app'
|
|
15
|
+
isolate_namespace ShopifyApp
|
|
16
|
+
|
|
17
|
+
initializer "shopify_app.assets.precompile" do |app|
|
|
18
|
+
app.config.assets.precompile += %w[
|
|
19
|
+
shopify_app/redirect.js
|
|
20
|
+
shopify_app/post_redirect.js
|
|
21
|
+
shopify_app/top_level.js
|
|
22
|
+
shopify_app/enable_cookies.js
|
|
23
|
+
shopify_app/request_storage_access.js
|
|
24
|
+
storage_access.svg
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
initializer "shopify_app.middleware" do |app|
|
|
29
|
+
app.config.middleware.insert_after(::Rack::Runtime, ShopifyApp::SameSiteCookieMiddleware)
|
|
30
|
+
|
|
31
|
+
if ShopifyApp.configuration.allow_jwt_authentication
|
|
32
|
+
app.config.middleware.insert_after(ShopifyApp::SameSiteCookieMiddleware, ShopifyApp::JWTMiddleware)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
initializer "shopify_app.redact_job_params" do
|
|
37
|
+
ActiveSupport.on_load(:active_job) do
|
|
38
|
+
if ActiveJob::Base.respond_to?(:log_arguments?)
|
|
39
|
+
WebhooksManagerJob.log_arguments = false
|
|
40
|
+
ScripttagsManagerJob.log_arguments = false
|
|
41
|
+
elsif ActiveJob::Logging::LogSubscriber.private_method_defined?(:args_info)
|
|
42
|
+
ActiveJob::Logging::LogSubscriber.prepend(RedactJobParams)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class ScripttagsManagerJob < ActiveJob::Base
|
|
4
|
+
queue_as do
|
|
5
|
+
ShopifyApp.configuration.scripttags_manager_queue_name
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def perform(shop_domain:, shop_token:, scripttags:)
|
|
9
|
+
api_version = ShopifyApp.configuration.api_version
|
|
10
|
+
ShopifyAPI::Session.temp(domain: shop_domain, token: shop_token, api_version: api_version) do
|
|
11
|
+
manager = ScripttagsManager.new(scripttags, shop_domain)
|
|
12
|
+
manager.create_scripttags
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class WebhooksManagerJob < ActiveJob::Base
|
|
4
|
+
queue_as do
|
|
5
|
+
ShopifyApp.configuration.webhooks_manager_queue_name
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def perform(shop_domain:, shop_token:, webhooks:)
|
|
9
|
+
api_version = ShopifyApp.configuration.api_version
|
|
10
|
+
ShopifyAPI::Session.temp(domain: shop_domain, token: shop_token, api_version: api_version) do
|
|
11
|
+
manager = WebhooksManager.new(webhooks)
|
|
12
|
+
manager.create_webhooks
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class ScripttagsManager
|
|
4
|
+
class CreationFailed < StandardError; end
|
|
5
|
+
|
|
6
|
+
def self.queue(shop_domain, shop_token, scripttags)
|
|
7
|
+
ShopifyApp::ScripttagsManagerJob.perform_later(
|
|
8
|
+
shop_domain: shop_domain,
|
|
9
|
+
shop_token: shop_token,
|
|
10
|
+
# Procs cannot be serialized so we interpolate now, if necessary
|
|
11
|
+
scripttags: build_src(scripttags, shop_domain)
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.build_src(scripttags, domain)
|
|
16
|
+
scripttags.map do |tag|
|
|
17
|
+
next tag unless tag[:src].respond_to?(:call)
|
|
18
|
+
tag = tag.dup
|
|
19
|
+
tag[:src] = tag[:src].call(domain)
|
|
20
|
+
tag
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :required_scripttags, :shop_domain
|
|
25
|
+
|
|
26
|
+
def initialize(scripttags, shop_domain)
|
|
27
|
+
@required_scripttags = scripttags
|
|
28
|
+
@shop_domain = shop_domain
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def recreate_scripttags!
|
|
32
|
+
destroy_scripttags
|
|
33
|
+
create_scripttags
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create_scripttags
|
|
37
|
+
return unless required_scripttags.present?
|
|
38
|
+
|
|
39
|
+
expanded_scripttags.each do |scripttag|
|
|
40
|
+
create_scripttag(scripttag) unless scripttag_exists?(scripttag[:src])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def destroy_scripttags
|
|
45
|
+
scripttags = expanded_scripttags
|
|
46
|
+
ShopifyAPI::ScriptTag.all.each do |tag|
|
|
47
|
+
ShopifyAPI::ScriptTag.delete(tag.id) if required_scripttag?(scripttags, tag)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@current_scripttags = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def expanded_scripttags
|
|
56
|
+
self.class.build_src(required_scripttags, shop_domain)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def required_scripttag?(scripttags, tag)
|
|
60
|
+
scripttags.map { |w| w[:src] }.include?(tag.src)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def create_scripttag(attributes)
|
|
64
|
+
attributes.reverse_merge!(format: 'json')
|
|
65
|
+
scripttag = ShopifyAPI::ScriptTag.create(attributes)
|
|
66
|
+
raise CreationFailed, scripttag.errors.full_messages.to_sentence unless scripttag.persisted?
|
|
67
|
+
scripttag
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def scripttag_exists?(src)
|
|
71
|
+
current_scripttags[src]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def current_scripttags
|
|
75
|
+
@current_scripttags ||= ShopifyAPI::ScriptTag.all.index_by(&:src)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class WebhooksManager
|
|
4
|
+
class CreationFailed < StandardError; end
|
|
5
|
+
|
|
6
|
+
def self.queue(shop_domain, shop_token, webhooks)
|
|
7
|
+
ShopifyApp::WebhooksManagerJob.perform_later(
|
|
8
|
+
shop_domain: shop_domain,
|
|
9
|
+
shop_token: shop_token,
|
|
10
|
+
webhooks: webhooks
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :required_webhooks
|
|
15
|
+
|
|
16
|
+
def initialize(webhooks)
|
|
17
|
+
@required_webhooks = webhooks
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def recreate_webhooks!
|
|
21
|
+
destroy_webhooks
|
|
22
|
+
create_webhooks
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_webhooks
|
|
26
|
+
return unless required_webhooks.present?
|
|
27
|
+
|
|
28
|
+
required_webhooks.each do |webhook|
|
|
29
|
+
create_webhook(webhook) unless webhook_exists?(webhook[:topic])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def destroy_webhooks
|
|
34
|
+
ShopifyAPI::Webhook.all.to_a.each do |webhook|
|
|
35
|
+
ShopifyAPI::Webhook.delete(webhook.id) if required_webhook?(webhook)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@current_webhooks = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def required_webhook?(webhook)
|
|
44
|
+
required_webhooks.map { |w| w[:address] }.include?(webhook.address)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_webhook(attributes)
|
|
48
|
+
attributes.reverse_merge!(format: 'json')
|
|
49
|
+
webhook = ShopifyAPI::Webhook.create(attributes)
|
|
50
|
+
raise CreationFailed, webhook.errors.full_messages.to_sentence unless webhook.persisted?
|
|
51
|
+
webhook
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def webhook_exists?(topic)
|
|
55
|
+
current_webhooks[topic]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def current_webhooks
|
|
59
|
+
@current_webhooks ||= ShopifyAPI::Webhook.all.to_a.index_by(&:topic)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
env['jwt.expire_at'] = jwt.expire_at
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class SameSiteCookieMiddleware
|
|
4
|
+
COOKIE_SEPARATOR = "\n"
|
|
5
|
+
|
|
6
|
+
def initialize(app)
|
|
7
|
+
@app = app
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
status, headers, body = @app.call(env)
|
|
12
|
+
user_agent = env['HTTP_USER_AGENT']
|
|
13
|
+
|
|
14
|
+
if headers && headers['Set-Cookie'] &&
|
|
15
|
+
BrowserSniffer.new(user_agent).same_site_none_compatible? &&
|
|
16
|
+
ShopifyApp.configuration.enable_same_site_none &&
|
|
17
|
+
Rack::Request.new(env).ssl?
|
|
18
|
+
|
|
19
|
+
set_cookies = headers['Set-Cookie']
|
|
20
|
+
.split(COOKIE_SEPARATOR)
|
|
21
|
+
.compact
|
|
22
|
+
.map do |cookie|
|
|
23
|
+
cookie << '; Secure' unless cookie =~ /;\s*secure/i
|
|
24
|
+
cookie << '; SameSite=None' if ShopifyApp.configuration.embedded_app?
|
|
25
|
+
cookie
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
headers['Set-Cookie'] = set_cookies.join(COOKIE_SEPARATOR)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
[status, headers, body]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
class OmniAuthConfiguration
|
|
5
|
+
attr_reader :strategy, :request
|
|
6
|
+
attr_writer :client_options_site, :scopes, :per_user_permissions
|
|
7
|
+
|
|
8
|
+
def initialize(strategy, request)
|
|
9
|
+
@strategy = strategy
|
|
10
|
+
@request = request
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build_options
|
|
14
|
+
strategy.options[:client_options][:site] = client_options_site
|
|
15
|
+
strategy.options[:scope] = scopes
|
|
16
|
+
strategy.options[:old_client_secret] = ShopifyApp.configuration.old_secret
|
|
17
|
+
strategy.options[:per_user_permissions] = request_online_tokens?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def request_online_tokens?
|
|
23
|
+
return @per_user_permissions unless @per_user_permissions.nil?
|
|
24
|
+
default_request_online_tokens?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def scopes
|
|
28
|
+
@scopes || default_scopes
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def client_options_site
|
|
32
|
+
@client_options_site || default_client_options_site
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def default_scopes
|
|
36
|
+
if request_online_tokens?
|
|
37
|
+
ShopifyApp.configuration.user_access_scopes
|
|
38
|
+
else
|
|
39
|
+
ShopifyApp.configuration.shop_access_scopes
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def default_client_options_site
|
|
44
|
+
return '' unless shop_domain.present?
|
|
45
|
+
"https://#{shopify_auth_params[:shop]}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def default_request_online_tokens?
|
|
49
|
+
strategy.session[:user_tokens] && !update_shop_scopes?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def update_shop_scopes?
|
|
53
|
+
ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(shop_domain)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def shop_domain
|
|
57
|
+
request.params['shop'] || (shopify_auth_params && shopify_auth_params['shop'])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def shopify_auth_params
|
|
61
|
+
strategy.session['shopify.omniauth_params']&.with_indifferent_access
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
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.
|
|
5
|
+
class InMemorySessionStore
|
|
6
|
+
class EnvironmentError < StandardError; end
|
|
7
|
+
|
|
8
|
+
def self.retrieve(id)
|
|
9
|
+
repo[id]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.store(session, *_args)
|
|
13
|
+
id = SecureRandom.uuid
|
|
14
|
+
repo[id] = session
|
|
15
|
+
id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.clear
|
|
19
|
+
@@repo = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.repo
|
|
23
|
+
if Rails.env.production?
|
|
24
|
+
raise EnvironmentError, "Cannot use InMemorySessionStore in a Production environment. \
|
|
25
|
+
Please initialize ShopifyApp with a model that can store and retrieve sessions"
|
|
26
|
+
end
|
|
27
|
+
@@repo ||= {}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
# rubocop:enable Style/ClassVars
|
|
31
|
+
end
|