shopify_app 22.0.1 → 22.2.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rubocop.yml +1 -2
  3. data/.rubocop.yml +0 -1
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile.lock +20 -17
  6. data/README.md +38 -0
  7. data/app/controllers/concerns/shopify_app/ensure_has_session.rb +11 -5
  8. data/app/controllers/concerns/shopify_app/ensure_installed.rb +8 -2
  9. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +5 -1
  10. data/app/controllers/shopify_app/callback_controller.rb +10 -1
  11. data/app/controllers/shopify_app/sessions_controller.rb +24 -4
  12. data/app/views/shopify_app/layouts/app_bridge.html.erb +17 -0
  13. data/app/views/shopify_app/sessions/patch_shopify_id_token.html.erb +0 -0
  14. data/config/routes.rb +1 -0
  15. data/docs/Troubleshooting.md +0 -23
  16. data/docs/Upgrading.md +25 -0
  17. data/docs/shopify_app/authentication.md +105 -20
  18. data/docs/shopify_app/sessions.md +110 -14
  19. data/docs/shopify_app/webhooks.md +1 -1
  20. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +2 -0
  21. data/lib/shopify_app/admin_api/with_token_refetch.rb +28 -0
  22. data/lib/shopify_app/auth/post_authenticate_tasks.rb +48 -0
  23. data/lib/shopify_app/auth/token_exchange.rb +73 -0
  24. data/lib/shopify_app/configuration.rb +54 -3
  25. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +1 -1
  26. data/lib/shopify_app/controller_concerns/embedded_app.rb +27 -0
  27. data/lib/shopify_app/controller_concerns/ensure_billing.rb +11 -3
  28. data/lib/shopify_app/controller_concerns/login_protection.rb +8 -23
  29. data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +5 -0
  30. data/lib/shopify_app/controller_concerns/token_exchange.rb +111 -0
  31. data/lib/shopify_app/controller_concerns/with_shopify_id_token.rb +41 -0
  32. data/lib/shopify_app/middleware/jwt_middleware.rb +13 -9
  33. data/lib/shopify_app/session/jwt.rb +9 -0
  34. data/lib/shopify_app/version.rb +1 -1
  35. data/lib/shopify_app.rb +9 -0
  36. data/package.json +1 -1
  37. data/shopify_app.gemspec +1 -1
  38. data/yarn.lock +3 -3
  39. metadata +12 -5
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module TokenExchange
5
+ extend ActiveSupport::Concern
6
+ include ShopifyApp::AdminAPI::WithTokenRefetch
7
+ include ShopifyApp::SanitizedParams
8
+ include ShopifyApp::EmbeddedApp
9
+
10
+ included do
11
+ include ShopifyApp::WithShopifyIdToken
12
+ end
13
+
14
+ INVALID_SHOPIFY_ID_TOKEN_ERRORS = [
15
+ ShopifyAPI::Errors::MissingJwtTokenError,
16
+ ShopifyAPI::Errors::InvalidJwtTokenError,
17
+ ].freeze
18
+
19
+ def activate_shopify_session(&block)
20
+ retrieve_session_from_token_exchange if current_shopify_session.blank? || should_exchange_expired_token?
21
+
22
+ ShopifyApp::Logger.debug("Activating Shopify session")
23
+ ShopifyAPI::Context.activate_session(current_shopify_session)
24
+ with_token_refetch(current_shopify_session, shopify_id_token, &block)
25
+ rescue *INVALID_SHOPIFY_ID_TOKEN_ERRORS => e
26
+ ShopifyApp::Logger.debug("Responding to invalid Shopify ID token: #{e.message}")
27
+ respond_to_invalid_shopify_id_token unless performed?
28
+ ensure
29
+ ShopifyApp::Logger.debug("Deactivating session")
30
+ ShopifyAPI::Context.deactivate_session
31
+ end
32
+
33
+ def should_exchange_expired_token?
34
+ ShopifyApp.configuration.check_session_expiry_date && current_shopify_session.expired?
35
+ end
36
+
37
+ def current_shopify_session
38
+ return unless current_shopify_session_id
39
+
40
+ @current_shopify_session ||= ShopifyApp::SessionRepository.load_session(current_shopify_session_id)
41
+ end
42
+
43
+ def current_shopify_session_id
44
+ @current_shopify_session_id ||= ShopifyAPI::Utils::SessionUtils.session_id_from_shopify_id_token(
45
+ id_token: shopify_id_token,
46
+ online: online_token_configured?,
47
+ )
48
+ end
49
+
50
+ def current_shopify_domain
51
+ sanitized_shop_name || current_shopify_session&.shop
52
+ end
53
+
54
+ private
55
+
56
+ def retrieve_session_from_token_exchange
57
+ @current_shopify_session = nil
58
+ ShopifyApp::Auth::TokenExchange.perform(shopify_id_token)
59
+ end
60
+
61
+ def respond_to_invalid_shopify_id_token
62
+ if request.headers["HTTP_AUTHORIZATION"].blank?
63
+ if missing_embedded_param?
64
+ redirect_to_embed_app_in_admin
65
+ else
66
+ redirect_to_bounce_page
67
+ end
68
+ else
69
+ ShopifyApp::Logger.debug("Responding to invalid Shopify ID token with unauthorized response")
70
+ response.set_header("X-Shopify-Retry-Invalid-Session-Request", 1)
71
+ unauthorized_response = { message: :unauthorized }
72
+ render(json: { errors: [unauthorized_response] }, status: :unauthorized)
73
+ end
74
+ end
75
+
76
+ def redirect_to_bounce_page
77
+ ShopifyApp::Logger.debug("Redirecting to bounce page for patching Shopify ID token")
78
+ patch_shopify_id_token_url =
79
+ "#{ShopifyAPI::Context.host}#{ShopifyApp.configuration.root_url}/patch_shopify_id_token"
80
+ patch_shopify_id_token_params = request.query_parameters.except(:id_token)
81
+
82
+ bounce_url = "#{request.path}?#{patch_shopify_id_token_params.to_query}"
83
+
84
+ # App Bridge will trigger a fetch to the URL in shopify-reload, with a new session token in headers
85
+ patch_shopify_id_token_params["shopify-reload"] = bounce_url
86
+
87
+ redirect_to(
88
+ "#{patch_shopify_id_token_url}?#{patch_shopify_id_token_params.to_query}",
89
+ allow_other_host: true,
90
+ )
91
+ end
92
+
93
+ def missing_embedded_param?
94
+ !params[:embedded].present? || params[:embedded] != "1"
95
+ end
96
+
97
+ def online_token_configured?
98
+ ShopifyApp.configuration.online_token_configured?
99
+ end
100
+
101
+ def fullpage_redirect_to(url)
102
+ raise ShopifyApp::ShopifyDomainNotFound if current_shopify_domain.nil?
103
+
104
+ render(
105
+ "shopify_app/shared/redirect",
106
+ layout: false,
107
+ locals: { url: url, current_shopify_domain: current_shopify_domain },
108
+ )
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module WithShopifyIdToken
5
+ extend ActiveSupport::Concern
6
+
7
+ def shopify_id_token
8
+ @shopify_id_token ||= id_token_from_request_env || id_token_from_authorization_header || id_token_from_url_param
9
+ end
10
+
11
+ def jwt_shopify_domain
12
+ request.env["jwt.shopify_domain"]
13
+ end
14
+
15
+ def jwt_shopify_user_id
16
+ request.env["jwt.shopify_user_id"]
17
+ end
18
+
19
+ def jwt_expire_at
20
+ expire_at = request.env["jwt.expire_at"]
21
+ return unless expire_at
22
+
23
+ expire_at - 5.seconds # 5s gap to start fetching new token in advance
24
+ end
25
+
26
+ private
27
+
28
+ def id_token_from_request_env
29
+ # This is set from ShopifyApp::JWTMiddleware
30
+ request.env["jwt.token"]
31
+ end
32
+
33
+ def id_token_from_authorization_header
34
+ request.headers["HTTP_AUTHORIZATION"]&.match(/^Bearer (.+)$/)&.[](1)
35
+ end
36
+
37
+ def id_token_from_url_param
38
+ params["id_token"]
39
+ end
40
+ end
41
+ end
@@ -2,16 +2,17 @@
2
2
 
3
3
  module ShopifyApp
4
4
  class JWTMiddleware
5
- TOKEN_REGEX = /^Bearer\s+(.*?)$/
5
+ TOKEN_REGEX = /^Bearer (.+)$/
6
+ ID_TOKEN_QUERY_PARAM = "id_token"
6
7
 
7
8
  def initialize(app)
8
9
  @app = app
9
10
  end
10
11
 
11
12
  def call(env)
12
- return call_next(env) unless authorization_header(env)
13
+ return call_next(env) unless ShopifyApp.configuration.embedded_app?
13
14
 
14
- token = extract_token(env)
15
+ token = token_from_authorization_header(env) || token_from_query_string(env)
15
16
  return call_next(env) unless token
16
17
 
17
18
  set_env_variables(token, env)
@@ -24,21 +25,24 @@ module ShopifyApp
24
25
  @app.call(env)
25
26
  end
26
27
 
27
- def authorization_header(env)
28
- env["HTTP_AUTHORIZATION"]
28
+ def token_from_authorization_header(env)
29
+ env["HTTP_AUTHORIZATION"]&.match(TOKEN_REGEX)&.[](1)
29
30
  end
30
31
 
31
- def extract_token(env)
32
- match = authorization_header(env).match(TOKEN_REGEX)
33
- match && match[1]
32
+ def token_from_query_string(env)
33
+ Rack::Utils.parse_nested_query(env["QUERY_STRING"])[ID_TOKEN_QUERY_PARAM]
34
34
  end
35
35
 
36
36
  def set_env_variables(token, env)
37
- jwt = ShopifyApp::JWT.new(token)
37
+ jwt = ShopifyAPI::Auth::JwtPayload.new(token)
38
38
 
39
+ env["jwt.token"] = token
39
40
  env["jwt.shopify_domain"] = jwt.shopify_domain
40
41
  env["jwt.shopify_user_id"] = jwt.shopify_user_id
41
42
  env["jwt.expire_at"] = jwt.expire_at
43
+ rescue ShopifyAPI::Errors::InvalidJwtTokenError
44
+ # ShopifyApp::JWT did not raise any exceptions, ensuring behaviour does not change
45
+ nil
42
46
  end
43
47
  end
44
48
  end
@@ -13,6 +13,7 @@ module ShopifyApp
13
13
  ]
14
14
 
15
15
  def initialize(token)
16
+ warn_deprecation
16
17
  @token = token
17
18
  set_payload
18
19
  end
@@ -60,5 +61,13 @@ module ShopifyApp
60
61
 
61
62
  payload
62
63
  end
64
+
65
+ def warn_deprecation
66
+ message = <<~EOS
67
+ "ShopifyApp::JWT will be deprecated, use ShopifyAPI::Auth::JwtPayload to parse JWT token instead."
68
+ EOS
69
+
70
+ ShopifyApp::Logger.deprecated(message, "23.0.0")
71
+ end
63
72
  end
64
73
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShopifyApp
4
- VERSION = "22.0.1"
4
+ VERSION = "22.2.0"
5
5
  end
data/lib/shopify_app.rb CHANGED
@@ -40,6 +40,9 @@ module ShopifyApp
40
40
 
41
41
  require "shopify_app/logger"
42
42
 
43
+ # Admin API helpers
44
+ require "shopify_app/admin_api/with_token_refetch"
45
+
43
46
  # controller concerns
44
47
  require "shopify_app/controller_concerns/csrf_protection"
45
48
  require "shopify_app/controller_concerns/localization"
@@ -52,6 +55,12 @@ module ShopifyApp
52
55
  require "shopify_app/controller_concerns/payload_verification"
53
56
  require "shopify_app/controller_concerns/app_proxy_verification"
54
57
  require "shopify_app/controller_concerns/webhook_verification"
58
+ require "shopify_app/controller_concerns/token_exchange"
59
+ require "shopify_app/controller_concerns/with_shopify_id_token"
60
+
61
+ # Auth helpers
62
+ require "shopify_app/auth/post_authenticate_tasks"
63
+ require "shopify_app/auth/token_exchange"
55
64
 
56
65
  # jobs
57
66
  require "shopify_app/jobs/webhooks_manager_job"
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify_app",
3
- "version": "22.0.1",
3
+ "version": "22.2.0",
4
4
  "repository": "git@github.com:Shopify/shopify_app.git",
5
5
  "author": "Shopify",
6
6
  "license": "MIT",
data/shopify_app.gemspec CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.add_runtime_dependency("jwt", ">= 2.2.3")
20
20
  s.add_runtime_dependency("rails", "> 5.2.1")
21
21
  s.add_runtime_dependency("redirect_safely", "~> 1.0")
22
- s.add_runtime_dependency("shopify_api", ">= 14.0.1", "< 15.0")
22
+ s.add_runtime_dependency("shopify_api", ">= 14.3.0", "< 15.0")
23
23
  s.add_runtime_dependency("sprockets-rails", ">= 2.0.0")
24
24
 
25
25
  s.add_development_dependency("byebug")
data/yarn.lock CHANGED
@@ -2040,9 +2040,9 @@ flatted@^3.2.7:
2040
2040
  integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
2041
2041
 
2042
2042
  follow-redirects@^1.0.0:
2043
- version "1.15.4"
2044
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
2045
- integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
2043
+ version "1.15.6"
2044
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
2045
+ integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
2046
2046
 
2047
2047
  fs-extra@^8.1.0:
2048
2048
  version "8.1.0"
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: 22.0.1
4
+ version: 22.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-12 00:00:00.000000000 Z
11
+ date: 2024-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activeresource
@@ -86,7 +86,7 @@ dependencies:
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: 14.0.1
89
+ version: 14.3.0
90
90
  - - "<"
91
91
  - !ruby/object:Gem::Version
92
92
  version: '15.0'
@@ -96,7 +96,7 @@ dependencies:
96
96
  requirements:
97
97
  - - ">="
98
98
  - !ruby/object:Gem::Version
99
- version: 14.0.1
99
+ version: 14.3.0
100
100
  - - "<"
101
101
  - !ruby/object:Gem::Version
102
102
  version: '15.0'
@@ -314,6 +314,7 @@ files:
314
314
  - app/controllers/shopify_app/extension_verification_controller.rb
315
315
  - app/controllers/shopify_app/sessions_controller.rb
316
316
  - app/controllers/shopify_app/webhooks_controller.rb
317
+ - app/views/shopify_app/layouts/app_bridge.html.erb
317
318
  - app/views/shopify_app/partials/_button_styles.html.erb
318
319
  - app/views/shopify_app/partials/_card_styles.html.erb
319
320
  - app/views/shopify_app/partials/_empty_state_styles.html.erb
@@ -321,6 +322,7 @@ files:
321
322
  - app/views/shopify_app/partials/_layout_styles.html.erb
322
323
  - app/views/shopify_app/partials/_typography_styles.html.erb
323
324
  - app/views/shopify_app/sessions/new.html.erb
325
+ - app/views/shopify_app/sessions/patch_shopify_id_token.html.erb
324
326
  - app/views/shopify_app/shared/redirect.html.erb
325
327
  - config/locales/cs.yml
326
328
  - config/locales/da.yml
@@ -414,6 +416,9 @@ files:
414
416
  - lib/shopify_app/access_scopes/noop_strategy.rb
415
417
  - lib/shopify_app/access_scopes/shop_strategy.rb
416
418
  - lib/shopify_app/access_scopes/user_strategy.rb
419
+ - lib/shopify_app/admin_api/with_token_refetch.rb
420
+ - lib/shopify_app/auth/post_authenticate_tasks.rb
421
+ - lib/shopify_app/auth/token_exchange.rb
417
422
  - lib/shopify_app/configuration.rb
418
423
  - lib/shopify_app/controller_concerns/app_proxy_verification.rb
419
424
  - lib/shopify_app/controller_concerns/csrf_protection.rb
@@ -425,7 +430,9 @@ files:
425
430
  - lib/shopify_app/controller_concerns/payload_verification.rb
426
431
  - lib/shopify_app/controller_concerns/redirect_for_embedded.rb
427
432
  - lib/shopify_app/controller_concerns/sanitized_params.rb
433
+ - lib/shopify_app/controller_concerns/token_exchange.rb
428
434
  - lib/shopify_app/controller_concerns/webhook_verification.rb
435
+ - lib/shopify_app/controller_concerns/with_shopify_id_token.rb
429
436
  - lib/shopify_app/engine.rb
430
437
  - lib/shopify_app/errors.rb
431
438
  - lib/shopify_app/jobs/webhooks_manager_job.rb
@@ -474,7 +481,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
474
481
  - !ruby/object:Gem::Version
475
482
  version: '0'
476
483
  requirements: []
477
- rubygems_version: 3.5.6
484
+ rubygems_version: 3.5.9
478
485
  signing_key:
479
486
  specification_version: 4
480
487
  summary: This gem is used to get quickly started with the Shopify API