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.
- checksums.yaml +4 -4
- data/.github/workflows/rubocop.yml +1 -2
- data/.rubocop.yml +0 -1
- data/CHANGELOG.md +30 -0
- data/Gemfile.lock +20 -17
- data/README.md +38 -0
- data/app/controllers/concerns/shopify_app/ensure_has_session.rb +11 -5
- data/app/controllers/concerns/shopify_app/ensure_installed.rb +8 -2
- data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +5 -1
- data/app/controllers/shopify_app/callback_controller.rb +10 -1
- data/app/controllers/shopify_app/sessions_controller.rb +24 -4
- data/app/views/shopify_app/layouts/app_bridge.html.erb +17 -0
- data/app/views/shopify_app/sessions/patch_shopify_id_token.html.erb +0 -0
- data/config/routes.rb +1 -0
- data/docs/Troubleshooting.md +0 -23
- data/docs/Upgrading.md +25 -0
- data/docs/shopify_app/authentication.md +105 -20
- data/docs/shopify_app/sessions.md +110 -14
- data/docs/shopify_app/webhooks.md +1 -1
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +2 -0
- data/lib/shopify_app/admin_api/with_token_refetch.rb +28 -0
- data/lib/shopify_app/auth/post_authenticate_tasks.rb +48 -0
- data/lib/shopify_app/auth/token_exchange.rb +73 -0
- data/lib/shopify_app/configuration.rb +54 -3
- data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +1 -1
- data/lib/shopify_app/controller_concerns/embedded_app.rb +27 -0
- data/lib/shopify_app/controller_concerns/ensure_billing.rb +11 -3
- data/lib/shopify_app/controller_concerns/login_protection.rb +8 -23
- data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +5 -0
- data/lib/shopify_app/controller_concerns/token_exchange.rb +111 -0
- data/lib/shopify_app/controller_concerns/with_shopify_id_token.rb +41 -0
- data/lib/shopify_app/middleware/jwt_middleware.rb +13 -9
- data/lib/shopify_app/session/jwt.rb +9 -0
- data/lib/shopify_app/version.rb +1 -1
- data/lib/shopify_app.rb +9 -0
- data/package.json +1 -1
- data/shopify_app.gemspec +1 -1
- data/yarn.lock +3 -3
- 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
|
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
|
13
|
+
return call_next(env) unless ShopifyApp.configuration.embedded_app?
|
13
14
|
|
14
|
-
token =
|
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
|
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
|
32
|
-
|
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 =
|
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
|
data/lib/shopify_app/version.rb
CHANGED
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
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
|
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.
|
2044
|
-
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.
|
2045
|
-
integrity sha512-
|
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
|
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-
|
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
|
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
|
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.
|
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
|