shopify_app 13.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|