shopify_app 13.0.1 → 13.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rubocop.yml +28 -0
- data/.rubocop.yml +2 -2
- data/.travis.yml +4 -3
- data/CHANGELOG.md +27 -0
- data/Gemfile +3 -1
- data/README.md +39 -42
- data/SECURITY.md +59 -0
- data/app/controllers/concerns/shopify_app/authenticated.rb +1 -0
- data/app/controllers/concerns/shopify_app/require_known_shop.rb +39 -0
- data/app/controllers/shopify_app/callback_controller.rb +38 -7
- data/app/controllers/shopify_app/extension_verification_controller.rb +2 -7
- data/app/controllers/shopify_app/sessions_controller.rb +1 -1
- data/app/controllers/shopify_app/webhooks_controller.rb +1 -1
- data/config/locales/nl.yml +7 -7
- data/docs/Quickstart.md +7 -17
- data/docs/Releasing.md +1 -0
- data/lib/generators/shopify_app/add_webhook/templates/{webhook_job.rb → webhook_job.rb.tt} +0 -0
- data/lib/generators/shopify_app/install/install_generator.rb +1 -1
- data/lib/generators/shopify_app/install/templates/flash_messages.js +0 -2
- data/lib/generators/shopify_app/install/templates/{shopify_app.rb → shopify_app.rb.tt} +1 -1
- data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +7 -3
- data/lib/generators/shopify_app/user_model/user_model_generator.rb +7 -3
- data/lib/shopify_app.rb +5 -1
- data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +0 -1
- data/lib/shopify_app/controller_concerns/csrf_protection.rb +15 -0
- data/lib/shopify_app/controller_concerns/login_protection.rb +14 -12
- data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
- data/lib/shopify_app/controller_concerns/webhook_verification.rb +1 -17
- data/lib/shopify_app/engine.rb +4 -0
- data/lib/shopify_app/middleware/jwt_middleware.rb +42 -0
- data/lib/shopify_app/session/jwt.rb +35 -22
- data/lib/shopify_app/test_helpers/all.rb +1 -0
- data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +2 -1
- data/lib/shopify_app/version.rb +1 -1
- data/package.json +1 -1
- data/shopify_app.gemspec +4 -3
- metadata +29 -9
@@ -7,6 +7,7 @@ module ShopifyApp
|
|
7
7
|
included do
|
8
8
|
include ShopifyApp::Localization
|
9
9
|
include ShopifyApp::LoginProtection
|
10
|
+
include ShopifyApp::CsrfProtection
|
10
11
|
include ShopifyApp::EmbeddedApp
|
11
12
|
before_action :login_again_if_different_user_or_shop
|
12
13
|
around_action :activate_shopify_session
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module RequireKnownShop
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action :check_shop_domain
|
9
|
+
before_action :check_shop_known
|
10
|
+
end
|
11
|
+
|
12
|
+
def current_shopify_domain
|
13
|
+
return if params[:shop].blank?
|
14
|
+
@shopify_domain ||= ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def check_shop_domain
|
20
|
+
redirect_to(ShopifyApp.configuration.login_url) unless current_shopify_domain
|
21
|
+
end
|
22
|
+
|
23
|
+
def check_shop_known
|
24
|
+
@shop = SessionRepository.retrieve_shop_session_by_shopify_domain(current_shopify_domain)
|
25
|
+
redirect_to(shop_login) unless @shop
|
26
|
+
end
|
27
|
+
|
28
|
+
def shop_login
|
29
|
+
url = URI(ShopifyApp.configuration.login_url)
|
30
|
+
|
31
|
+
url.query = URI.encode_www_form(
|
32
|
+
shop: params[:shop],
|
33
|
+
return_to: request.fullpath,
|
34
|
+
)
|
35
|
+
|
36
|
+
url.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -6,10 +6,22 @@ module ShopifyApp
|
|
6
6
|
include ShopifyApp::LoginProtection
|
7
7
|
|
8
8
|
def callback
|
9
|
-
|
10
|
-
|
9
|
+
unless auth_hash
|
10
|
+
return respond_with_error
|
11
|
+
end
|
12
|
+
|
13
|
+
if jwt_request? && !valid_jwt_auth?
|
14
|
+
return respond_with_error
|
15
|
+
end
|
16
|
+
|
17
|
+
if jwt_request?
|
18
|
+
set_shopify_session
|
19
|
+
head(:ok)
|
20
|
+
else
|
21
|
+
reset_session_options
|
22
|
+
set_shopify_session
|
11
23
|
|
12
|
-
if
|
24
|
+
if redirect_for_user_token?
|
13
25
|
return redirect_to(login_url_with_optional_shop)
|
14
26
|
end
|
15
27
|
|
@@ -18,17 +30,30 @@ module ShopifyApp
|
|
18
30
|
perform_after_authenticate_job
|
19
31
|
|
20
32
|
redirect_to(return_address)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def respond_with_error
|
39
|
+
if jwt_request?
|
40
|
+
head(:unauthorized)
|
21
41
|
else
|
22
42
|
flash[:error] = I18n.t('could_not_log_in')
|
23
43
|
redirect_to(login_url_with_optional_shop)
|
24
44
|
end
|
25
45
|
end
|
26
46
|
|
27
|
-
|
47
|
+
def redirect_for_user_token?
|
48
|
+
ShopifyApp::SessionRepository.user_storage.present? && user_session.blank?
|
49
|
+
end
|
28
50
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
51
|
+
def jwt_request?
|
52
|
+
jwt_shopify_domain || jwt_shopify_user_id
|
53
|
+
end
|
54
|
+
|
55
|
+
def valid_jwt_auth?
|
56
|
+
auth_hash && jwt_shopify_domain == shop_name && jwt_shopify_user_id == associated_user_id
|
32
57
|
end
|
33
58
|
|
34
59
|
def auth_hash
|
@@ -45,6 +70,10 @@ module ShopifyApp
|
|
45
70
|
auth_hash['extra']['associated_user']
|
46
71
|
end
|
47
72
|
|
73
|
+
def associated_user_id
|
74
|
+
associated_user && associated_user['id']
|
75
|
+
end
|
76
|
+
|
48
77
|
def token
|
49
78
|
auth_hash['credentials']['token']
|
50
79
|
end
|
@@ -63,9 +92,11 @@ module ShopifyApp
|
|
63
92
|
|
64
93
|
session[:shopify_user] = associated_user
|
65
94
|
if session[:shopify_user].present?
|
95
|
+
session[:shop_id] = nil if shop_session && shop_session.domain != shop_name
|
66
96
|
session[:user_id] = ShopifyApp::SessionRepository.store_user_session(session_store, associated_user)
|
67
97
|
else
|
68
98
|
session[:shop_id] = ShopifyApp::SessionRepository.store_shop_session(session_store)
|
99
|
+
session[:user_id] = nil if user_session && user_session.domain != shop_name
|
69
100
|
end
|
70
101
|
session[:shopify_domain] = shop_name
|
71
102
|
session[:user_session] = auth_hash&.extra&.session
|
@@ -2,19 +2,14 @@
|
|
2
2
|
|
3
3
|
module ShopifyApp
|
4
4
|
class ExtensionVerificationController < ActionController::Base
|
5
|
+
include ShopifyApp::PayloadVerification
|
5
6
|
protect_from_forgery with: :null_session
|
6
7
|
before_action :verify_request
|
7
8
|
|
8
9
|
private
|
9
10
|
|
10
11
|
def verify_request
|
11
|
-
|
12
|
-
request_body = request.body.read
|
13
|
-
secret = ShopifyApp.configuration.secret
|
14
|
-
digest = OpenSSL::Digest.new('sha256')
|
15
|
-
|
16
|
-
expected_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, request_body))
|
17
|
-
head(:unauthorized) unless ActiveSupport::SecurityUtils.secure_compare(expected_hmac, hmac_header)
|
12
|
+
head(:unauthorized) unless hmac_valid?(request.body.read)
|
18
13
|
end
|
19
14
|
end
|
20
15
|
end
|
@@ -125,7 +125,7 @@ module ShopifyApp
|
|
125
125
|
end
|
126
126
|
|
127
127
|
def copy_return_to_param_to_session
|
128
|
-
session[:return_to] = params[:return_to] if params[:return_to]
|
128
|
+
session[:return_to] = RedirectSafely.make_safe(params[:return_to], '/') if params[:return_to]
|
129
129
|
end
|
130
130
|
|
131
131
|
def render_invalid_shop_error
|
data/config/locales/nl.yml
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
---
|
2
2
|
nl:
|
3
|
-
logged_out:
|
3
|
+
logged_out: Je bent afgemeld
|
4
4
|
could_not_log_in: Kon niet aanmelden bij Shopify-winkel
|
5
5
|
invalid_shop_url: Ongeldig winkeldomein
|
6
6
|
enable_cookies_heading: Schakel cookies in van %{app}
|
7
|
-
enable_cookies_body:
|
7
|
+
enable_cookies_body: Je moet cookies in deze browser handmatig inschakelen om %{app}
|
8
8
|
binnen Shopify te gebruiken.
|
9
|
-
enable_cookies_footer: Met cookies kan de app
|
9
|
+
enable_cookies_footer: Met cookies kan de app je verifiëren door je voorkeuren en
|
10
10
|
persoonlijke informatie tijdelijk op te slaan. Ze vervallen na 30 dagen.
|
11
11
|
enable_cookies_action: Schakel cookies in
|
12
|
-
top_level_interaction_heading:
|
13
|
-
top_level_interaction_body:
|
14
|
-
te vragen tot cookies voordat Shopify het voor
|
12
|
+
top_level_interaction_heading: Je browser moet %{app} verifiëren
|
13
|
+
top_level_interaction_body: Je browser heeft apps nodig zoals %{app} om je toegang
|
14
|
+
te vragen tot cookies voordat Shopify het voor je kan openen.
|
15
15
|
top_level_interaction_action: Doorgaan
|
16
16
|
request_storage_access_heading: "%{app} heeft toegang tot cookies nodig"
|
17
|
-
request_storage_access_body: Hiermee kan de app
|
17
|
+
request_storage_access_body: Hiermee kan de app je verifiëren door je persoonlijke
|
18
18
|
gegevens tijdelijk op te slaan. Klik op Doorgaan en sta cookies toe om de app
|
19
19
|
te gebruiken.
|
20
20
|
request_storage_access_footer: Cookies verlopen na 30 dagen.
|
data/docs/Quickstart.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
Quickstart
|
2
2
|
==========
|
3
3
|
|
4
|
-
Get started building and deploying a new Shopify App to Heroku in just a few minutes.
|
4
|
+
Get started building and deploying a new Shopify App to Heroku in just a few minutes.
|
5
|
+
This guide assumes you have Ruby, Rails and PostgreSQL installed on your computer already; if you haven't done that already start with [this guide.](https://guides.rubyonrails.org/v5.0/getting_started.html#installing-rails)
|
5
6
|
|
6
7
|
1. New Rails App (with postgres)
|
7
8
|
--------------------------------
|
@@ -26,15 +27,6 @@ Head to the Heroku dashboard and create a new app, or run the following commands
|
|
26
27
|
CLI:
|
27
28
|
```sh
|
28
29
|
$ heroku create name
|
29
|
-
$ heroku git:remote -a name
|
30
|
-
```
|
31
|
-
|
32
|
-
Once you have created an app on Heroku, we need to let Git know where the Heroku server is so we can deploy to it later. Copy the app's name from your Heroku dashboard and substitute `appname.git` with the name you chose earlier:
|
33
|
-
|
34
|
-
web:
|
35
|
-
```sh
|
36
|
-
# https://dashboard.heroku.com/new
|
37
|
-
$ git remote add heroku git@heroku.com:appname.git
|
38
30
|
```
|
39
31
|
|
40
32
|
3. Create a new App in the Shopify Partner dashboard
|
@@ -48,11 +40,10 @@ $ git remote add heroku git@heroku.com:appname.git
|
|
48
40
|
4. Add ShopifyApp to Gemfile
|
49
41
|
----------------------------
|
50
42
|
|
51
|
-
Run
|
43
|
+
Run this command to add the `shopify_app` Gem to your app:
|
52
44
|
|
53
45
|
```sh
|
54
|
-
$
|
55
|
-
$ bundle install
|
46
|
+
$ bundle add shopify_app
|
56
47
|
```
|
57
48
|
|
58
49
|
**Note:** we recommend using the latest version of Shopify Gem. Check the [Git tags](https://github.com/Shopify/shopify_app/tags) to see the latest release version and then add it to your Gemfile e.g `gem 'shopify_app', '~> 7.0.0'`
|
@@ -64,14 +55,13 @@ Generate the code for your app by running these commands:
|
|
64
55
|
|
65
56
|
```sh
|
66
57
|
# Use the keys from your app you created in the partners area
|
67
|
-
$ rails generate shopify_app
|
58
|
+
$ rails generate shopify_app
|
68
59
|
$ git add .
|
69
60
|
$ git commit -m 'generated shopify app'
|
70
61
|
```
|
71
62
|
|
72
|
-
|
73
|
-
|
74
|
-
We recommend adding a gem or utilizing environment variables (`.env`) to handle your keys before releasing your app. [Learn more about using environment variables.](https://www.honeybadger.io/blog/ruby-guide-environment-variables/)
|
63
|
+
Your API key and secret are read from environment variables. Refer to the main
|
64
|
+
README for further details on how to set this up.
|
75
65
|
|
76
66
|
6. Deploy your app
|
77
67
|
---------
|
data/docs/Releasing.md
CHANGED
@@ -3,6 +3,7 @@ Releasing ShopifyApp
|
|
3
3
|
1. Check the Semantic Versioning page for info on how to version the new release: http://semver.org
|
4
4
|
2. Create a pull request with the following changes:
|
5
5
|
* Update the version of ShopifyApp in lib/shopify_app/version.rb
|
6
|
+
* Update the version of shopify_app in package.json
|
6
7
|
* Add a CHANGELOG entry for the new release with the date
|
7
8
|
* Change the title of the PR to something like: "Packaging for release X.Y.Z"
|
8
9
|
3. Merge your pull request
|
File without changes
|
@@ -46,7 +46,7 @@ module ShopifyApp
|
|
46
46
|
copy_file('shopify_app.js', 'app/javascript/shopify_app/shopify_app.js')
|
47
47
|
copy_file('flash_messages.js', 'app/javascript/shopify_app/flash_messages.js')
|
48
48
|
copy_file('shopify_app_index.js', 'app/javascript/shopify_app/index.js')
|
49
|
-
append_to_file('app/javascript/packs/application.js',
|
49
|
+
append_to_file('app/javascript/packs/application.js', "require(\"shopify_app\")\n")
|
50
50
|
else
|
51
51
|
copy_file('shopify_app.js', 'app/assets/javascripts/shopify_app.js')
|
52
52
|
copy_file('flash_messages.js', 'app/assets/javascripts/flash_messages.js')
|
@@ -8,7 +8,7 @@ ShopifyApp.configure do |config|
|
|
8
8
|
config.embedded_app = <%= embedded_app? %>
|
9
9
|
config.after_authenticate_job = false
|
10
10
|
config.api_version = "<%= @api_version %>"
|
11
|
-
config.shop_session_repository = '
|
11
|
+
config.shop_session_repository = 'Shop'
|
12
12
|
end
|
13
13
|
|
14
14
|
# ShopifyApp::Utils.fetch_known_api_versions # Uncomment to fetch known api versions from shopify servers on boot
|
@@ -30,9 +30,13 @@ module ShopifyApp
|
|
30
30
|
Rails.version.match(/\d\.\d/)[0]
|
31
31
|
end
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
class << self
|
34
|
+
private :next_migration_number
|
35
|
+
|
36
|
+
# for generating a timestamp when using `create_migration`
|
37
|
+
def next_migration_number(dir)
|
38
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
39
|
+
end
|
36
40
|
end
|
37
41
|
end
|
38
42
|
end
|
@@ -30,9 +30,13 @@ module ShopifyApp
|
|
30
30
|
Rails.version.match(/\d\.\d/)[0]
|
31
31
|
end
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
class << self
|
34
|
+
private :next_migration_number
|
35
|
+
|
36
|
+
# for generating a timestamp when using `create_migration`
|
37
|
+
def next_migration_number(dir)
|
38
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
39
|
+
end
|
36
40
|
end
|
37
41
|
end
|
38
42
|
end
|
data/lib/shopify_app.rb
CHANGED
@@ -4,6 +4,7 @@ require 'shopify_app/version'
|
|
4
4
|
# deps
|
5
5
|
require 'shopify_api'
|
6
6
|
require 'omniauth-shopify-oauth2'
|
7
|
+
require 'redirect_safely'
|
7
8
|
|
8
9
|
module ShopifyApp
|
9
10
|
def self.rails6?
|
@@ -26,12 +27,14 @@ module ShopifyApp
|
|
26
27
|
require 'shopify_app/utils'
|
27
28
|
|
28
29
|
# controller concerns
|
30
|
+
require 'shopify_app/controller_concerns/csrf_protection'
|
29
31
|
require 'shopify_app/controller_concerns/localization'
|
30
32
|
require 'shopify_app/controller_concerns/itp'
|
31
33
|
require 'shopify_app/controller_concerns/login_protection'
|
32
34
|
require 'shopify_app/controller_concerns/embedded_app'
|
33
|
-
require 'shopify_app/controller_concerns/
|
35
|
+
require 'shopify_app/controller_concerns/payload_verification'
|
34
36
|
require 'shopify_app/controller_concerns/app_proxy_verification'
|
37
|
+
require 'shopify_app/controller_concerns/webhook_verification'
|
35
38
|
|
36
39
|
# jobs
|
37
40
|
require 'shopify_app/jobs/webhooks_manager_job'
|
@@ -42,6 +45,7 @@ module ShopifyApp
|
|
42
45
|
require 'shopify_app/managers/scripttags_manager'
|
43
46
|
|
44
47
|
# middleware
|
48
|
+
require 'shopify_app/middleware/jwt_middleware'
|
45
49
|
require 'shopify_app/middleware/same_site_cookie_middleware'
|
46
50
|
|
47
51
|
# session
|
@@ -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
|
@@ -83,17 +83,11 @@ module ShopifyApp
|
|
83
83
|
protected
|
84
84
|
|
85
85
|
def jwt_shopify_domain
|
86
|
-
|
87
|
-
@jwt_shopify_domain ||= JWT.new(jwt).shopify_domain
|
86
|
+
request.env['jwt.shopify_domain']
|
88
87
|
end
|
89
88
|
|
90
89
|
def jwt_shopify_user_id
|
91
|
-
|
92
|
-
@jwt_user_id ||= JWT.new(jwt).shopify_user_id
|
93
|
-
end
|
94
|
-
|
95
|
-
def jwt
|
96
|
-
@jwt ||= authenticate_with_http_token { |token| token }
|
90
|
+
request.env['jwt.shopify_user_id']
|
97
91
|
end
|
98
92
|
|
99
93
|
def redirect_to_login
|
@@ -108,7 +102,7 @@ module ShopifyApp
|
|
108
102
|
path = referer.path
|
109
103
|
query = "#{referer.query}&#{sanitized_params.to_query}"
|
110
104
|
end
|
111
|
-
session[:return_to] = "#{path}?#{query}"
|
105
|
+
session[:return_to] = query.blank? ? path.to_s : "#{path}?#{query}"
|
112
106
|
redirect_to(login_url_with_optional_shop)
|
113
107
|
end
|
114
108
|
end
|
@@ -139,7 +133,7 @@ module ShopifyApp
|
|
139
133
|
query_params = {}
|
140
134
|
query_params[:shop] = sanitized_params[:shop] if params[:shop].present?
|
141
135
|
|
142
|
-
return_to = session[:return_to] || params[:return_to]
|
136
|
+
return_to = RedirectSafely.make_safe(session[:return_to] || params[:return_to], nil)
|
143
137
|
|
144
138
|
if return_to.present? && return_to_param_required?
|
145
139
|
query_params[:return_to] = return_to
|
@@ -170,7 +164,10 @@ module ShopifyApp
|
|
170
164
|
end
|
171
165
|
|
172
166
|
def current_shopify_domain
|
173
|
-
shopify_domain = sanitized_shop_name ||
|
167
|
+
shopify_domain = sanitized_shop_name ||
|
168
|
+
jwt_shopify_domain ||
|
169
|
+
session[:shopify_domain]
|
170
|
+
|
174
171
|
return shopify_domain if shopify_domain.present?
|
175
172
|
|
176
173
|
raise ShopifyDomainNotFound
|
@@ -205,11 +202,16 @@ module ShopifyApp
|
|
205
202
|
end
|
206
203
|
|
207
204
|
def return_address
|
205
|
+
return base_return_address unless ShopifyApp.configuration.allow_jwt_authentication
|
206
|
+
return_address_with_params(shop: current_shopify_domain)
|
207
|
+
end
|
208
|
+
|
209
|
+
def base_return_address
|
208
210
|
session.delete(:return_to) || ShopifyApp.configuration.root_url
|
209
211
|
end
|
210
212
|
|
211
213
|
def return_address_with_params(params)
|
212
|
-
uri = URI(
|
214
|
+
uri = URI(base_return_address)
|
213
215
|
uri.query = CGI.parse(uri.query.to_s)
|
214
216
|
.symbolize_keys
|
215
217
|
.transform_values { |v| v.one? ? v.first : v }
|