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.
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
  - - ">="