shopify_app 22.1.0 → 22.2.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/Gemfile.lock +3 -3
- data/README.md +38 -0
- data/app/controllers/concerns/shopify_app/ensure_installed.rb +4 -3
- data/app/controllers/shopify_app/sessions_controller.rb +5 -1
- 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 +14 -0
- data/docs/shopify_app/authentication.md +69 -6
- data/docs/shopify_app/sessions.md +110 -14
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +2 -0
- data/lib/shopify_app/admin_api/with_token_refetch.rb +27 -0
- data/lib/shopify_app/auth/token_exchange.rb +73 -0
- data/lib/shopify_app/configuration.rb +3 -3
- 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 +7 -22
- data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +5 -0
- data/lib/shopify_app/controller_concerns/token_exchange.rb +75 -105
- 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 +5 -0
- data/package.json +1 -1
- data/shopify_app.gemspec +1 -1
- metadata +10 -5
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module Auth
|
5
|
+
class TokenExchange
|
6
|
+
attr_reader :id_token
|
7
|
+
|
8
|
+
def self.perform(id_token)
|
9
|
+
new(id_token).perform
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(id_token)
|
13
|
+
@id_token = id_token
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform
|
17
|
+
domain = ShopifyAPI::Auth::JwtPayload.new(id_token).shopify_domain
|
18
|
+
|
19
|
+
Logger.info("Performing Token Exchange for [#{domain}] - (Offline)")
|
20
|
+
session = exchange_token(
|
21
|
+
shop: domain,
|
22
|
+
id_token: id_token,
|
23
|
+
requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN,
|
24
|
+
)
|
25
|
+
|
26
|
+
if online_token_configured?
|
27
|
+
Logger.info("Performing Token Exchange for [#{domain}] - (Online)")
|
28
|
+
session = exchange_token(
|
29
|
+
shop: domain,
|
30
|
+
id_token: id_token,
|
31
|
+
requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN,
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
ShopifyApp.configuration.post_authenticate_tasks.perform(session)
|
36
|
+
|
37
|
+
session
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def exchange_token(shop:, id_token:, requested_token_type:)
|
43
|
+
session = ShopifyAPI::Auth::TokenExchange.exchange_token(
|
44
|
+
shop: shop,
|
45
|
+
session_token: id_token,
|
46
|
+
requested_token_type: requested_token_type,
|
47
|
+
)
|
48
|
+
|
49
|
+
SessionRepository.store_session(session)
|
50
|
+
|
51
|
+
session
|
52
|
+
rescue ShopifyAPI::Errors::InvalidJwtTokenError
|
53
|
+
Logger.error("Invalid id token '#{id_token}' during token exchange")
|
54
|
+
raise
|
55
|
+
rescue ShopifyAPI::Errors::HttpResponseError => error
|
56
|
+
Logger.error(
|
57
|
+
"A #{error.code} error (#{error.class}) occurred during the token exchange. Response: #{error.response.body}",
|
58
|
+
)
|
59
|
+
raise
|
60
|
+
rescue ActiveRecord::RecordNotUnique
|
61
|
+
Logger.debug("Session not stored due to concurrent token exchange calls")
|
62
|
+
session
|
63
|
+
rescue => error
|
64
|
+
Logger.error("An error occurred during the token exchange: [#{error.class}] #{error.message}")
|
65
|
+
raise
|
66
|
+
end
|
67
|
+
|
68
|
+
def online_token_configured?
|
69
|
+
ShopifyApp.configuration.online_token_configured?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -48,8 +48,8 @@ module ShopifyApp
|
|
48
48
|
# takes a ShopifyApp::BillingConfiguration object
|
49
49
|
attr_accessor :billing
|
50
50
|
|
51
|
-
#
|
52
|
-
attr_accessor :
|
51
|
+
# Enables new authorization flow using token exchange
|
52
|
+
attr_accessor :new_embedded_auth_strategy
|
53
53
|
|
54
54
|
def initialize
|
55
55
|
@root_url = "/"
|
@@ -129,7 +129,7 @@ module ShopifyApp
|
|
129
129
|
end
|
130
130
|
|
131
131
|
def use_new_embedded_auth_strategy?
|
132
|
-
|
132
|
+
new_embedded_auth_strategy && embedded_app?
|
133
133
|
end
|
134
134
|
|
135
135
|
def online_token_configured?
|
@@ -5,6 +5,7 @@ module ShopifyApp
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
include ShopifyApp::FrameAncestors
|
8
|
+
include ShopifyApp::SanitizedParams
|
8
9
|
|
9
10
|
included do
|
10
11
|
layout :embedded_app_layout
|
@@ -13,6 +14,22 @@ module ShopifyApp
|
|
13
14
|
|
14
15
|
protected
|
15
16
|
|
17
|
+
def redirect_to_embed_app_in_admin
|
18
|
+
ShopifyApp::Logger.debug("Redirecting to embed app in admin")
|
19
|
+
|
20
|
+
host = if params[:host]
|
21
|
+
params[:host]
|
22
|
+
elsif params[:shop]
|
23
|
+
Base64.encode64("#{sanitized_shop_name}/admin")
|
24
|
+
else
|
25
|
+
return redirect_to(ShopifyApp.configuration.login_url)
|
26
|
+
end
|
27
|
+
|
28
|
+
redirect_path = ShopifyAPI::Auth.embedded_app_url(host)
|
29
|
+
redirect_path = ShopifyApp.configuration.root_url if deduced_phishing_attack?(redirect_path)
|
30
|
+
redirect_to(redirect_path, allow_other_host: true)
|
31
|
+
end
|
32
|
+
|
16
33
|
def use_embedded_app_layout?
|
17
34
|
ShopifyApp.configuration.embedded_app?
|
18
35
|
end
|
@@ -27,5 +44,15 @@ module ShopifyApp
|
|
27
44
|
response.set_header("P3P", 'CP="Not used"')
|
28
45
|
response.headers.except!("X-Frame-Options")
|
29
46
|
end
|
47
|
+
|
48
|
+
def deduced_phishing_attack?(decoded_host)
|
49
|
+
sanitized_host = ShopifyApp::Utils.sanitize_shop_domain(decoded_host)
|
50
|
+
if sanitized_host.nil?
|
51
|
+
message = "Host param for redirect to embed app in admin is not from a trusted domain, " \
|
52
|
+
"redirecting to root as this is likely a phishing attack."
|
53
|
+
ShopifyApp::Logger.info(message)
|
54
|
+
end
|
55
|
+
sanitized_host.nil?
|
56
|
+
end
|
30
57
|
end
|
31
58
|
end
|
@@ -27,7 +27,7 @@ module ShopifyApp
|
|
27
27
|
|
28
28
|
unless has_payment
|
29
29
|
if request.xhr?
|
30
|
-
|
30
|
+
RedirectForEmbedded.add_app_bridge_redirect_url_header(confirmation_url, response)
|
31
31
|
ShopifyApp::Logger.debug("Responding with 401 unauthorized")
|
32
32
|
head(:unauthorized)
|
33
33
|
elsif ShopifyApp.configuration.embedded_app?
|
@@ -45,8 +45,16 @@ module ShopifyApp
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def handle_billing_error(error)
|
48
|
-
|
49
|
-
|
48
|
+
ShopifyApp::Logger.warn("Encountered billing error - #{error.message}: #{error.errors}\n" \
|
49
|
+
"Redirecting to login page")
|
50
|
+
|
51
|
+
login_url = ShopifyApp.configuration.login_url
|
52
|
+
if request.xhr?
|
53
|
+
RedirectForEmbedded.add_app_bridge_redirect_url_header(login_url, response)
|
54
|
+
head(:unauthorized)
|
55
|
+
else
|
56
|
+
fullpage_redirect_to(login_url)
|
57
|
+
end
|
50
58
|
end
|
51
59
|
|
52
60
|
def has_active_payment?(session)
|
@@ -16,6 +16,7 @@ module ShopifyApp
|
|
16
16
|
end
|
17
17
|
|
18
18
|
rescue_from ShopifyAPI::Errors::HttpResponseError, with: :handle_http_error
|
19
|
+
include ShopifyApp::WithShopifyIdToken
|
19
20
|
end
|
20
21
|
|
21
22
|
ACCESS_TOKEN_REQUIRED_HEADER = "X-Shopify-API-Request-Failure-Unauthorized"
|
@@ -53,7 +54,7 @@ module ShopifyApp
|
|
53
54
|
@current_shopify_session ||= begin
|
54
55
|
cookie_name = ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME
|
55
56
|
load_current_session(
|
56
|
-
|
57
|
+
shopify_id_token: shopify_id_token,
|
57
58
|
cookies: { cookie_name => cookies.encrypted[cookie_name] },
|
58
59
|
is_online: online_token_configured?,
|
59
60
|
)
|
@@ -78,13 +79,6 @@ module ShopifyApp
|
|
78
79
|
response.set_header(ACCESS_TOKEN_REQUIRED_HEADER, "true")
|
79
80
|
end
|
80
81
|
|
81
|
-
def jwt_expire_at
|
82
|
-
expire_at = request.env["jwt.expire_at"]
|
83
|
-
return unless expire_at
|
84
|
-
|
85
|
-
expire_at - 5.seconds # 5s gap to start fetching new token in advance
|
86
|
-
end
|
87
|
-
|
88
82
|
def add_top_level_redirection_headers(url: nil, ignore_response_code: false)
|
89
83
|
if request.xhr? && (ignore_response_code || response.code.to_i == 401)
|
90
84
|
ShopifyApp::Logger.debug("Adding top level redirection headers")
|
@@ -94,8 +88,8 @@ module ShopifyApp
|
|
94
88
|
params[:shop] = if current_shopify_session
|
95
89
|
current_shopify_session.shop
|
96
90
|
|
97
|
-
elsif
|
98
|
-
jwt_payload = ShopifyAPI::Auth::JwtPayload.new(
|
91
|
+
elsif shopify_id_token
|
92
|
+
jwt_payload = ShopifyAPI::Auth::JwtPayload.new(shopify_id_token)
|
99
93
|
jwt_payload.shop
|
100
94
|
end
|
101
95
|
end
|
@@ -103,21 +97,12 @@ module ShopifyApp
|
|
103
97
|
url ||= login_url_with_optional_shop
|
104
98
|
|
105
99
|
ShopifyApp::Logger.debug("Setting Reauthorize-Url to #{url}")
|
106
|
-
|
107
|
-
response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", url)
|
100
|
+
RedirectForEmbedded.add_app_bridge_redirect_url_header(url, response)
|
108
101
|
end
|
109
102
|
end
|
110
103
|
|
111
104
|
protected
|
112
105
|
|
113
|
-
def jwt_shopify_domain
|
114
|
-
request.env["jwt.shopify_domain"]
|
115
|
-
end
|
116
|
-
|
117
|
-
def jwt_shopify_user_id
|
118
|
-
request.env["jwt.shopify_user_id"]
|
119
|
-
end
|
120
|
-
|
121
106
|
def host
|
122
107
|
params[:host]
|
123
108
|
end
|
@@ -273,10 +258,10 @@ module ShopifyApp
|
|
273
258
|
online_token_configured?
|
274
259
|
end
|
275
260
|
|
276
|
-
def load_current_session(
|
261
|
+
def load_current_session(shopify_id_token: nil, cookies: nil, is_online: false)
|
277
262
|
return ShopifyAPI::Context.load_private_session if ShopifyAPI::Context.private?
|
278
263
|
|
279
|
-
session_id = ShopifyAPI::Utils::SessionUtils.current_session_id(
|
264
|
+
session_id = ShopifyAPI::Utils::SessionUtils.current_session_id(shopify_id_token, cookies, is_online)
|
280
265
|
return nil unless session_id
|
281
266
|
|
282
267
|
ShopifyApp::SessionRepository.load_session(session_id)
|
@@ -4,6 +4,11 @@ module ShopifyApp
|
|
4
4
|
module RedirectForEmbedded
|
5
5
|
include ShopifyApp::SanitizedParams
|
6
6
|
|
7
|
+
def self.add_app_bridge_redirect_url_header(url, response)
|
8
|
+
response.set_header("X-Shopify-API-Request-Failure-Reauthorize", "1")
|
9
|
+
response.set_header("X-Shopify-API-Request-Failure-Reauthorize-Url", url)
|
10
|
+
end
|
11
|
+
|
7
12
|
private
|
8
13
|
|
9
14
|
def embedded_redirect_url?
|
@@ -3,139 +3,109 @@
|
|
3
3
|
module ShopifyApp
|
4
4
|
module TokenExchange
|
5
5
|
extend ActiveSupport::Concern
|
6
|
+
include ShopifyApp::AdminAPI::WithTokenRefetch
|
7
|
+
include ShopifyApp::SanitizedParams
|
8
|
+
include ShopifyApp::EmbeddedApp
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
10
|
+
included do
|
11
|
+
include ShopifyApp::WithShopifyIdToken
|
12
|
+
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
16
32
|
|
17
|
-
|
18
|
-
|
19
|
-
ShopifyAPI::Context.activate_session(current_shopify_session)
|
20
|
-
yield
|
21
|
-
ensure
|
22
|
-
ShopifyApp::Logger.debug("Deactivating session")
|
23
|
-
ShopifyAPI::Context.deactivate_session
|
24
|
-
end
|
33
|
+
def should_exchange_expired_token?
|
34
|
+
ShopifyApp.configuration.check_session_expiry_date && current_shopify_session.expired?
|
25
35
|
end
|
26
36
|
|
27
37
|
def current_shopify_session
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
nil,
|
32
|
-
online_token_configured?,
|
33
|
-
)
|
34
|
-
return nil unless session_id
|
35
|
-
|
36
|
-
ShopifyApp::SessionRepository.load_session(session_id)
|
37
|
-
end
|
38
|
+
return unless current_shopify_session_id
|
39
|
+
|
40
|
+
@current_shopify_session ||= ShopifyApp::SessionRepository.load_session(current_shopify_session_id)
|
38
41
|
end
|
39
42
|
|
40
|
-
def
|
41
|
-
|
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
|
42
49
|
|
43
|
-
|
50
|
+
def current_shopify_domain
|
51
|
+
sanitized_shop_name || current_shopify_session&.shop
|
44
52
|
end
|
45
53
|
|
46
54
|
private
|
47
55
|
|
48
56
|
def retrieve_session_from_token_exchange
|
49
|
-
|
50
|
-
|
51
|
-
# we need to update the middleware to also update the env['jwt.shopify_domain'] from the query params
|
52
|
-
domain = ShopifyApp::JWT.new(session_token).shopify_domain
|
53
|
-
|
54
|
-
ShopifyApp::Logger.info("Performing Token Exchange for [#{domain}] - (Offline)")
|
55
|
-
session = exchange_token(
|
56
|
-
shop: domain, # TODO: use jwt_shopify_domain ?
|
57
|
-
session_token: session_token,
|
58
|
-
requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::OFFLINE_ACCESS_TOKEN,
|
59
|
-
)
|
60
|
-
|
61
|
-
if session && online_token_configured?
|
62
|
-
ShopifyApp::Logger.info("Performing Token Exchange for [#{domain}] - (Online)")
|
63
|
-
session = exchange_token(
|
64
|
-
shop: domain, # TODO: use jwt_shopify_domain ?
|
65
|
-
session_token: session_token,
|
66
|
-
requested_token_type: ShopifyAPI::Auth::TokenExchange::RequestedTokenType::ONLINE_ACCESS_TOKEN,
|
67
|
-
)
|
68
|
-
end
|
69
|
-
|
70
|
-
ShopifyApp.configuration.post_authenticate_tasks.perform(session)
|
57
|
+
@current_shopify_session = nil
|
58
|
+
ShopifyApp::Auth::TokenExchange.perform(shopify_id_token)
|
71
59
|
end
|
72
60
|
|
73
|
-
def
|
74
|
-
if
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
begin
|
80
|
-
session = ShopifyAPI::Auth::TokenExchange.exchange_token(
|
81
|
-
shop: shop,
|
82
|
-
session_token: session_token,
|
83
|
-
requested_token_type: requested_token_type,
|
84
|
-
)
|
85
|
-
rescue ShopifyAPI::Errors::InvalidJwtTokenError
|
86
|
-
# respond_to_invalid_session_token
|
87
|
-
return
|
88
|
-
rescue ShopifyAPI::Errors::HttpResponseError => error
|
89
|
-
ShopifyApp::Logger.error(
|
90
|
-
"A #{error.code} error (#{error.class}) occurred during the token exchange. Response: #{error.response.body}",
|
91
|
-
)
|
92
|
-
raise
|
93
|
-
rescue => error
|
94
|
-
ShopifyApp::Logger.error("An error occurred during the token exchange: #{error.message}")
|
95
|
-
raise
|
96
|
-
end
|
97
|
-
|
98
|
-
if session
|
99
|
-
begin
|
100
|
-
ShopifyApp::SessionRepository.store_session(session)
|
101
|
-
rescue ActiveRecord::RecordNotUnique
|
102
|
-
ShopifyApp::Logger.debug("Session not stored due to concurrent token exchange calls")
|
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
|
103
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)
|
104
73
|
end
|
105
|
-
|
106
|
-
session
|
107
|
-
end
|
108
|
-
|
109
|
-
def session_token
|
110
|
-
@session_token ||= id_token_header
|
111
74
|
end
|
112
75
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
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)
|
116
81
|
|
117
|
-
|
118
|
-
# TODO: Implement this method to handle invalid session tokens
|
82
|
+
bounce_url = "#{request.path}?#{patch_shopify_id_token_params.to_query}"
|
119
83
|
|
120
|
-
#
|
121
|
-
|
122
|
-
# unauthorized_response = { message: :unauthorized }
|
123
|
-
# render(json: { errors: [unauthorized_response] }, status: :unauthorized)
|
124
|
-
# else
|
125
|
-
# patch_session_token_url = "#{ShopifyAPI::Context.host}/patch_session_token"
|
126
|
-
# patch_session_token_params = request.query_parameters.except(:id_token)
|
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
|
127
86
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
87
|
+
redirect_to(
|
88
|
+
"#{patch_shopify_id_token_url}?#{patch_shopify_id_token_params.to_query}",
|
89
|
+
allow_other_host: true,
|
90
|
+
)
|
91
|
+
end
|
132
92
|
|
133
|
-
|
134
|
-
|
93
|
+
def missing_embedded_param?
|
94
|
+
!params[:embedded].present? || params[:embedded] != "1"
|
135
95
|
end
|
136
96
|
|
137
97
|
def online_token_configured?
|
138
98
|
ShopifyApp.configuration.online_token_configured?
|
139
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
|
140
110
|
end
|
141
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"
|
@@ -53,9 +56,11 @@ module ShopifyApp
|
|
53
56
|
require "shopify_app/controller_concerns/app_proxy_verification"
|
54
57
|
require "shopify_app/controller_concerns/webhook_verification"
|
55
58
|
require "shopify_app/controller_concerns/token_exchange"
|
59
|
+
require "shopify_app/controller_concerns/with_shopify_id_token"
|
56
60
|
|
57
61
|
# Auth helpers
|
58
62
|
require "shopify_app/auth/post_authenticate_tasks"
|
63
|
+
require "shopify_app/auth/token_exchange"
|
59
64
|
|
60
65
|
# jobs
|
61
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.
|
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")
|