shopify_app 13.0.1 → 13.4.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yml +28 -0
  3. data/.rubocop.yml +2 -2
  4. data/.travis.yml +4 -3
  5. data/CHANGELOG.md +27 -0
  6. data/Gemfile +3 -1
  7. data/README.md +39 -42
  8. data/SECURITY.md +59 -0
  9. data/app/controllers/concerns/shopify_app/authenticated.rb +1 -0
  10. data/app/controllers/concerns/shopify_app/require_known_shop.rb +39 -0
  11. data/app/controllers/shopify_app/callback_controller.rb +38 -7
  12. data/app/controllers/shopify_app/extension_verification_controller.rb +2 -7
  13. data/app/controllers/shopify_app/sessions_controller.rb +1 -1
  14. data/app/controllers/shopify_app/webhooks_controller.rb +1 -1
  15. data/config/locales/nl.yml +7 -7
  16. data/docs/Quickstart.md +7 -17
  17. data/docs/Releasing.md +1 -0
  18. data/lib/generators/shopify_app/add_webhook/templates/{webhook_job.rb → webhook_job.rb.tt} +0 -0
  19. data/lib/generators/shopify_app/install/install_generator.rb +1 -1
  20. data/lib/generators/shopify_app/install/templates/flash_messages.js +0 -2
  21. data/lib/generators/shopify_app/install/templates/{shopify_app.rb → shopify_app.rb.tt} +1 -1
  22. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +7 -3
  23. data/lib/generators/shopify_app/user_model/user_model_generator.rb +7 -3
  24. data/lib/shopify_app.rb +5 -1
  25. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +0 -1
  26. data/lib/shopify_app/controller_concerns/csrf_protection.rb +15 -0
  27. data/lib/shopify_app/controller_concerns/login_protection.rb +14 -12
  28. data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
  29. data/lib/shopify_app/controller_concerns/webhook_verification.rb +1 -17
  30. data/lib/shopify_app/engine.rb +4 -0
  31. data/lib/shopify_app/middleware/jwt_middleware.rb +42 -0
  32. data/lib/shopify_app/session/jwt.rb +35 -22
  33. data/lib/shopify_app/test_helpers/all.rb +1 -0
  34. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +2 -1
  35. data/lib/shopify_app/version.rb +1 -1
  36. data/package.json +1 -1
  37. data/shopify_app.gemspec +4 -3
  38. metadata +29 -9
@@ -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
@@ -2,6 +2,7 @@
2
2
  module ShopifyApp
3
3
  module WebhookVerification
4
4
  extend ActiveSupport::Concern
5
+ include ShopifyApp::PayloadVerification
5
6
 
6
7
  included do
7
8
  skip_before_action :verify_authenticity_token, raise: false
@@ -15,25 +16,8 @@ module ShopifyApp
15
16
  return head(:unauthorized) unless hmac_valid?(data)
16
17
  end
17
18
 
18
- def hmac_valid?(data)
19
- secrets = [ShopifyApp.configuration.secret, ShopifyApp.configuration.old_secret].reject(&:blank?)
20
-
21
- secrets.any? do |secret|
22
- digest = OpenSSL::Digest.new('sha256')
23
-
24
- ActiveSupport::SecurityUtils.secure_compare(
25
- shopify_hmac,
26
- Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, data))
27
- )
28
- end
29
- end
30
-
31
19
  def shop_domain
32
20
  request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN']
33
21
  end
34
-
35
- def shopify_hmac
36
- request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']
37
- end
38
22
  end
39
23
  end
@@ -16,6 +16,10 @@ module ShopifyApp
16
16
 
17
17
  initializer "shopify_app.middleware" do |app|
18
18
  app.config.middleware.insert_after(::Rack::Runtime, ShopifyApp::SameSiteCookieMiddleware)
19
+
20
+ if ShopifyApp.configuration.allow_jwt_authentication
21
+ app.config.middleware.insert_after(ShopifyApp::SameSiteCookieMiddleware, ShopifyApp::JWTMiddleware)
22
+ end
19
23
  end
20
24
  end
21
25
  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
@@ -1,48 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
  module ShopifyApp
3
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
+
4
18
  def initialize(token)
5
19
  @token = token
6
20
  set_payload
7
21
  end
8
22
 
9
23
  def shopify_domain
10
- payload && dest_host
24
+ @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['dest'])
11
25
  end
12
26
 
13
27
  def shopify_user_id
14
- payload && payload['sub']
28
+ @payload && @payload['sub']
15
29
  end
16
30
 
17
31
  private
18
32
 
19
- def payload
20
- return unless @payload
21
- return unless dest_host
22
- return unless dest_host == iss_host
23
- return unless @payload['aud'] == ShopifyApp.configuration.api_key
24
-
25
- @payload
26
- end
27
-
28
33
  def set_payload
29
- @payload, _ = parse_token_data(ShopifyApp.configuration.secret)
30
- configuration_old_secret = @payload && ShopifyApp.configuration
31
- @payload, _ = parse_token_data(ShopifyApp.configuration.old_secret) unless configuration_old_secret
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
32
39
  end
33
40
 
34
- def parse_token_data(secret)
41
+ def parse_token_data(secret, old_secret)
35
42
  ::JWT.decode(@token, secret, true, { algorithm: 'HS256' })
36
- rescue ::JWT::DecodeError, ::JWT::VerificationError, ::JWT::ExpiredSignature, ::JWT::ImmatureSignature
37
- nil
38
- end
43
+ rescue ::JWT::VerificationError
44
+ raise unless old_secret
39
45
 
40
- def dest_host
41
- @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['dest'])
46
+ ::JWT.decode(@token, old_secret, true, { algorithm: 'HS256' })
42
47
  end
43
48
 
44
- def iss_host
45
- @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['iss'])
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
46
59
  end
47
60
  end
48
61
  end
@@ -1 +1,2 @@
1
+ # frozen_string_literal: true
1
2
  require 'shopify_app/test_helpers/webhook_verification_helper'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module TestHelpers
3
4
  module WebhookVerificationHelper
@@ -8,7 +9,7 @@ module ShopifyApp
8
9
  @request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] = valid_hmac
9
10
  end
10
11
 
11
- def unauthorized_webhook_verification_headers!(params = {})
12
+ def unauthorized_webhook_verification_headers!
12
13
  @request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] = "invalid_hmac"
13
14
  end
14
15
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ShopifyApp
3
- VERSION = '13.0.1'
3
+ VERSION = '13.4.0'
4
4
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify_app",
3
- "version": "13.0.1",
3
+ "version": "13.4.0",
4
4
  "repository": "git@github.com:Shopify/shopify_app.git",
5
5
  "author": "Shopify",
6
6
  "license": "MIT",
@@ -9,15 +9,16 @@ Gem::Specification.new do |s|
9
9
  s.author = "Shopify"
10
10
  s.summary = 'This gem is used to get quickly started with the Shopify API'
11
11
 
12
- s.required_ruby_version = ">= 2.3.1"
12
+ s.required_ruby_version = ">= 2.4"
13
13
 
14
14
  s.metadata['allowed_push_host'] = 'https://rubygems.org'
15
15
 
16
- s.add_runtime_dependency('browser_sniffer', '~> 1.2.0')
16
+ s.add_runtime_dependency('browser_sniffer', '~> 1.2.2')
17
17
  s.add_runtime_dependency('rails', '> 5.2.1')
18
- s.add_runtime_dependency('shopify_api', '~> 9.0.2')
18
+ s.add_runtime_dependency('shopify_api', '~> 9.1.0')
19
19
  s.add_runtime_dependency('omniauth-shopify-oauth2', '~> 2.2.2')
20
20
  s.add_runtime_dependency('jwt', '~> 2.2.1')
21
+ s.add_runtime_dependency('redirect_safely', '~> 1.0')
21
22
 
22
23
  s.add_development_dependency('rake')
23
24
  s.add_development_dependency('byebug')
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify_app
3
3
  version: !ruby/object:Gem::Version
4
- version: 13.0.1
4
+ version: 13.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-14 00:00:00.000000000 Z
11
+ date: 2020-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: browser_sniffer
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.2.0
19
+ version: 1.2.2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.2.0
26
+ version: 1.2.2
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rails
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 9.0.2
47
+ version: 9.1.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 9.0.2
54
+ version: 9.1.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: omniauth-shopify-oauth2
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 2.2.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: redirect_safely
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rake
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -230,6 +244,7 @@ files:
230
244
  - ".github/CODEOWNERS"
231
245
  - ".github/ISSUE_TEMPLATE.md"
232
246
  - ".github/probots.yml"
247
+ - ".github/workflows/rubocop.yml"
233
248
  - ".gitignore"
234
249
  - ".nvmrc"
235
250
  - ".rubocop.yml"
@@ -240,6 +255,7 @@ files:
240
255
  - LICENSE
241
256
  - README.md
242
257
  - Rakefile
258
+ - SECURITY.md
243
259
  - app/assets/images/storage_access.svg
244
260
  - app/assets/javascripts/shopify_app/enable_cookies.js
245
261
  - app/assets/javascripts/shopify_app/itp_helper.js
@@ -251,6 +267,7 @@ files:
251
267
  - app/assets/javascripts/shopify_app/top_level.js
252
268
  - app/assets/javascripts/shopify_app/top_level_interaction.js
253
269
  - app/controllers/concerns/shopify_app/authenticated.rb
270
+ - app/controllers/concerns/shopify_app/require_known_shop.rb
254
271
  - app/controllers/shopify_app/authenticated_controller.rb
255
272
  - app/controllers/shopify_app/callback_controller.rb
256
273
  - app/controllers/shopify_app/extension_verification_controller.rb
@@ -301,7 +318,7 @@ files:
301
318
  - lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb
302
319
  - lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb
303
320
  - lib/generators/shopify_app/add_webhook/add_webhook_generator.rb
304
- - lib/generators/shopify_app/add_webhook/templates/webhook_job.rb
321
+ - lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt
305
322
  - lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb
306
323
  - lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb
307
324
  - lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb
@@ -319,7 +336,7 @@ files:
319
336
  - lib/generators/shopify_app/install/templates/omniauth.rb
320
337
  - lib/generators/shopify_app/install/templates/session_store.rb
321
338
  - lib/generators/shopify_app/install/templates/shopify_app.js
322
- - lib/generators/shopify_app/install/templates/shopify_app.rb
339
+ - lib/generators/shopify_app/install/templates/shopify_app.rb.tt
323
340
  - lib/generators/shopify_app/install/templates/shopify_app_index.js
324
341
  - lib/generators/shopify_app/install/templates/shopify_provider.rb
325
342
  - lib/generators/shopify_app/install/templates/user_agent.rb
@@ -341,16 +358,19 @@ files:
341
358
  - lib/shopify_app.rb
342
359
  - lib/shopify_app/configuration.rb
343
360
  - lib/shopify_app/controller_concerns/app_proxy_verification.rb
361
+ - lib/shopify_app/controller_concerns/csrf_protection.rb
344
362
  - lib/shopify_app/controller_concerns/embedded_app.rb
345
363
  - lib/shopify_app/controller_concerns/itp.rb
346
364
  - lib/shopify_app/controller_concerns/localization.rb
347
365
  - lib/shopify_app/controller_concerns/login_protection.rb
366
+ - lib/shopify_app/controller_concerns/payload_verification.rb
348
367
  - lib/shopify_app/controller_concerns/webhook_verification.rb
349
368
  - lib/shopify_app/engine.rb
350
369
  - lib/shopify_app/jobs/scripttags_manager_job.rb
351
370
  - lib/shopify_app/jobs/webhooks_manager_job.rb
352
371
  - lib/shopify_app/managers/scripttags_manager.rb
353
372
  - lib/shopify_app/managers/webhooks_manager.rb
373
+ - lib/shopify_app/middleware/jwt_middleware.rb
354
374
  - lib/shopify_app/middleware/same_site_cookie_middleware.rb
355
375
  - lib/shopify_app/session/in_memory_session_store.rb
356
376
  - lib/shopify_app/session/in_memory_shop_session_store.rb
@@ -385,7 +405,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
385
405
  requirements:
386
406
  - - ">="
387
407
  - !ruby/object:Gem::Version
388
- version: 2.3.1
408
+ version: '2.4'
389
409
  required_rubygems_version: !ruby/object:Gem::Requirement
390
410
  requirements:
391
411
  - - ">="