shopify_app 22.0.1 → 22.2.0

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