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,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,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
|
|
@@ -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' unless cookie =~ /;\s*samesite=/i
|
|
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,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
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
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
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
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
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class JWT
|
|
4
|
+
class InvalidDestinationError < StandardError; end
|
|
5
|
+
class MismatchedHostsError < StandardError; end
|
|
6
|
+
class InvalidAudienceError < StandardError; end
|
|
7
|
+
|
|
8
|
+
WARN_EXCEPTIONS = [
|
|
9
|
+
::JWT::DecodeError,
|
|
10
|
+
::JWT::ExpiredSignature,
|
|
11
|
+
::JWT::ImmatureSignature,
|
|
12
|
+
::JWT::VerificationError,
|
|
13
|
+
InvalidAudienceError,
|
|
14
|
+
InvalidDestinationError,
|
|
15
|
+
MismatchedHostsError,
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
def initialize(token)
|
|
19
|
+
@token = token
|
|
20
|
+
set_payload
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def shopify_domain
|
|
24
|
+
@payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['dest'])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def shopify_user_id
|
|
28
|
+
@payload && @payload['sub']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def set_payload
|
|
34
|
+
payload, _ = parse_token_data(ShopifyApp.configuration&.secret, ShopifyApp.configuration&.old_secret)
|
|
35
|
+
@payload = validate_payload(payload)
|
|
36
|
+
rescue *WARN_EXCEPTIONS => error
|
|
37
|
+
Rails.logger.warn("[ShopifyApp::JWT] Failed to validate JWT: [#{error.class}] #{error}")
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_token_data(secret, old_secret)
|
|
42
|
+
::JWT.decode(@token, secret, true, { algorithm: 'HS256' })
|
|
43
|
+
rescue ::JWT::VerificationError
|
|
44
|
+
raise unless old_secret
|
|
45
|
+
|
|
46
|
+
::JWT.decode(@token, old_secret, true, { algorithm: 'HS256' })
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_payload(payload)
|
|
50
|
+
dest_host = ShopifyApp::Utils.sanitize_shop_domain(payload['dest'])
|
|
51
|
+
iss_host = ShopifyApp::Utils.sanitize_shop_domain(payload['iss'])
|
|
52
|
+
api_key = ShopifyApp.configuration.api_key
|
|
53
|
+
|
|
54
|
+
raise InvalidAudienceError, "'aud' claim does not match api_key" unless payload['aud'] == api_key
|
|
55
|
+
raise InvalidDestinationError, "'dest' claim host not a valid shopify host" unless dest_host
|
|
56
|
+
raise MismatchedHostsError, "'dest' claim host does not match 'iss' claim host" unless dest_host == iss_host
|
|
57
|
+
|
|
58
|
+
payload
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class NullUserSessionStore
|
|
4
|
+
class << self
|
|
5
|
+
def retrieve(_)
|
|
6
|
+
nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def store(_, _)
|
|
10
|
+
raise SessionRepository::ConfigurationError, 'user_storage is not configured'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def retrieve_by_shopify_user_id(_)
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def blank?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class SessionRepository
|
|
4
|
+
class ConfigurationError < StandardError; end
|
|
5
|
+
|
|
6
|
+
class << self
|
|
7
|
+
attr_writer :shop_storage
|
|
8
|
+
|
|
9
|
+
attr_writer :user_storage
|
|
10
|
+
|
|
11
|
+
def retrieve_shop_session(id)
|
|
12
|
+
shop_storage.retrieve(id)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def retrieve_user_session(id)
|
|
16
|
+
user_storage.retrieve(id)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def retrieve_shop_session_by_shopify_domain(shopify_domain)
|
|
20
|
+
shop_storage.retrieve_by_shopify_domain(shopify_domain)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def retrieve_user_session_by_shopify_user_id(user_id)
|
|
24
|
+
user_storage.retrieve_by_shopify_user_id(user_id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def store_shop_session(session)
|
|
28
|
+
shop_storage.store(session)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def store_user_session(session, user)
|
|
32
|
+
user_storage.store(session, user)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def shop_storage
|
|
36
|
+
load_shop_storage || raise(ConfigurationError, "ShopifySessionRepository.shop_storage is not configured!")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def user_storage
|
|
40
|
+
load_user_storage
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def load_shop_storage
|
|
46
|
+
return unless @shop_storage
|
|
47
|
+
@shop_storage.respond_to?(:safe_constantize) ? @shop_storage.safe_constantize : @shop_storage
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load_user_storage
|
|
51
|
+
return NullUserSessionStore unless @user_storage
|
|
52
|
+
@user_storage.respond_to?(:safe_constantize) ? @user_storage.safe_constantize : @user_storage
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|