shopify_app 22.1.0 → 22.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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")
|