Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EnsureBilling and EnsureAuthenticatedLinks works with token exchange #1833

Merged
merged 9 commits into from Apr 29, 2024

Conversation

zzooeeyy
Copy link
Contributor

@zzooeeyy zzooeeyy commented Apr 25, 2024

What this PR does

  • Making sure EnsureBilling and EnsureAuthenticatedLinks can call methods defined in token exchange, by extracting some common helper methods for re-use
  • Fixed an infinite redirect loop caused by handling billing API error -
    • Steps to reprduce:
      1. Open app
      2. EnsureHasSession checks if billing is valid
      3. Billing API responds with billing error
      4. EnsureBilling handles billing error by redirecting to shop /login with shop and host params
      5. /login starts auth again
      6. Auth CallbackController handles the callback, but it also checks billing, which could still return error - thus redirecting back to /login again. ↺
    • Fix was to only redirect to shop /login withOUT shop and host params so that /login does not re-start auth.
  • TokenExchangeConcern -
    • If id_session is invalid, redirect to embed the app first before redirecting to bounce page.

Before - Billing API error sent app into infinite redirect loop

infinite-loop-billing-error.mp4

After - Billing API error does not send app into infinite redirect loop

redirect-to-login.mp4

Token exchange works with EnsureBIlling and can load app after confirming charge

token-exchange-confirm-charge.mp4

Checklist

Before submitting the PR, please consider if any of the following are needed:

  • Update CHANGELOG.md if the changes would impact users
  • Update README.md, if appropriate.
  • Update any relevant pages in /docs, if necessary
  • For security fixes, the Disclosure Policy must be followed.

@zzooeeyy zzooeeyy force-pushed the ensure-other-concerns-work branch 2 times, most recently from 25026be to 4efdfb2 Compare April 25, 2024 17:46
@@ -45,8 +45,16 @@ def billing_required?
end

def handle_billing_error(error)
logger.info("#{error.message}: #{error.errors}")
redirect_to_login
Copy link
Contributor Author

@zzooeeyy zzooeeyy Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was causing an infinite redirect loop in auth code flow because the shop param gets added to the login path, and CallbackController would re-check billing on auto login with the shop param..

Changing the logic to redirect to login page without shop param.

end

def add_top_level_redirection_headers(url: nil, ignore_response_code: false)
def add_top_level_redirection_headers(ignore_response_code: false)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url argument is not used anywhere anymore, removing this.
Extracted helper RedirectForEmbedded.add_app_bridge_redirect_url_header(url, response) to be reused elsewhere

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is something apps can use, right? Otherwise we might be removing functionality they need.

Although I guess in that case they could just use the other concern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm yea, although this method added more checks around when to add the headers. I think I'll just add the existing behaviour back to avoid breaking behaviour for apps

Comment on lines -82 to -87
def jwt_expire_at
expire_at = request.env["jwt.expire_at"]
return unless expire_at

expire_at - 5.seconds # 5s gap to start fetching new token in advance
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted these 'jwt' helpers to be inside WithShopifyIdToken concern for reusability

Comment on lines -114 to -120
def jwt_shopify_domain
request.env["jwt.shopify_domain"]
end

def jwt_shopify_user_id
request.env["jwt.shopify_user_id"]
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted these 'jwt' helpers to be inside WithShopifyIdToken concern for reusability

@zzooeeyy zzooeeyy marked this pull request as ready for review April 26, 2024 14:35
@zzooeeyy zzooeeyy requested a review from a team as a code owner April 26, 2024 14:36
end

def add_top_level_redirection_headers(url: nil, ignore_response_code: false)
def add_top_level_redirection_headers(ignore_response_code: false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is something apps can use, right? Otherwise we might be removing functionality they need.

Although I guess in that case they could just use the other concern.

!params[:embedded].present? || params[:embedded] != "1"
end

def redirect_to_embed_app
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity - won't we have other places in the code that need to do this as well? Seems to me like this might also be extractable if that's the case.

host = if params[:host]
params[:host]
elsif params[:shop]
Base64.encode64("#{sanitized_shop_name}/admin")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little bit of an assumption - I think there are some cases where we might not want this (requests aren't coming from the admin), but we can deal with those if / when they happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure if I understand? If they're using the EnsureHasSession concern, they want to validate requests that are coming from the admin right?

Copy link
Contributor

@paulomarg paulomarg Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are cases where host isn't necessarily shop/admin, so we're basically assuming that will be the case whenever we hit this path. Which I think should be fine and in the unlikely event that this fails on any specific interaction, we can look into it later.

end

test "#jwt_expire_at returns jwt.expire_at - 5 seconds from request env" do
expected_expire_at = Time.now.to_i
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we locking down the time here? I wonder if this could lead to flaky tests if the second happens to shift during the run.

Copy link
Contributor

@paulomarg paulomarg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking.

@zzooeeyy zzooeeyy merged commit 9e53256 into main Apr 29, 2024
7 checks passed
@zzooeeyy zzooeeyy deleted the ensure-other-concerns-work branch April 29, 2024 22:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants