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

Add new auth strategy docs & set token exchange as default configuration from rails generator #1834

Merged
merged 14 commits into from
May 2, 2024

Conversation

zzooeeyy
Copy link
Contributor

@zzooeeyy zzooeeyy commented Apr 29, 2024

What this PR does

Before and after with the configuration on/off

  • The embedded app is using previous OAuth with callbacks in "authorization code grant" flow when config.new_embedded_auth_strategy = false.
  • The embedded app is using the new zero redirect OAuth with "token exchange" flow when config.new_embedded_auth_strategy = true

New app install

Auth code flow (old) New embedded app auth (new)
auth-code-flow-install.mp4
token-exchange-install.mp4
There are multiple redirects back and forth from app to Shopify to retrieve both the offline and online access tokens. Took about 15s from "install" to home page being displayed. There are no redirects happening at all from the app after the install. Home page displayed in about 5s

Adding new access scope

Auth code flow (old) New embedded app auth (new)
auth-code-flow-new-scope-720.mov
token-exchange-new-scope.mp4
When a new scope has been deployed, the old authorization code flow can't pick it up until the server has been restarted. Then the user has to go through many redirects to get the new access tokens with the new access scopes. When a new scope has been deployed, the new scope grant will be automatically prompted. Once the user grants approval to the new scopes, the operation will continue.

Expired online access token

Auth code flow (old) New embedded app auth (new)
auth-code-flow-expired-online-token.mp4
token-exchange-expired-online-token.mp4
When the online access token has expired. The app redirected multiple times to retrieve the new access tokens, and the XHR operation from the front end didn't get completed. When the online access token has expired, the new auth strategy automatically fetches the new access token before handling the actual API request with no redirects.

Invalidated online access token

Auth code flow (old) New embedded app auth (new)
auth-code-flow-invalid-access-token.mp4
token-exchange-invalid-access-token.mp4
When the online access token has been invalidated, the app could not handle XHR requests until the app has been reloaded and redirected multiple times for OAuth to retrieve new access tokens When the online access token has been invalidated, the server handles the 401 HTTP unauthorized error and fetches new access token through token exchange before retrying the controller action again. The result is a seamless server side handling of this error with zero redirects.

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 add-docs branch 2 times, most recently from b3bd298 to a8e5e4e Compare April 29, 2024 23:01
@zzooeeyy zzooeeyy changed the title Add docs Add new auth strategy docs & set token exchange as default configuration from rails generator Apr 29, 2024
@zzooeeyy zzooeeyy marked this pull request as ready for review April 30, 2024 23:56
@zzooeeyy zzooeeyy requested a review from a team as a code owner April 30, 2024 23:56
@@ -4,6 +4,8 @@ ShopifyApp.configure do |config|
config.scope = "<%= @scope %>" # Consult this page for more scope options:
# https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes
config.embedded_app = <%= embedded_app? %>
config.new_embedded_auth_strategy = <%= embedded_app? %>
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we realized we need to also enable token expiration check to use the new strategy, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

If that's the case, would it make sense to turn that on in the background, and fail if an invalid combination is explicitly provided?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea I thought about enabling it, but I thought it might be confusing for apps that aren't using online tokens....
And Paulo brings up a good point, I'm not sure why we have this check anyways, it should just not get a new token automatically if there are no online tokens or if online token doesn't have an expiry date column, and wouldn't need a config for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And I just double checked, if the app has a User table but they don't have "expire_at" column, with the check enabled, it'll actually raise an NotImplementedError.. 🤔
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And I just found this change to the template -- Shopify/shopify-app-template-ruby#24

Looks offline tokens are preferred instead of using online token?

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.

I had a few thoughts, but nothing blocking.

README.md Outdated
@@ -129,6 +129,50 @@ These routes are configurable. See the more detailed [*Engine*](/docs/shopify_ap

To learn more about how this gem authenticates with Shopify, see [*Authentication*](/docs/shopify_app/authentication.md).

### New embedded app authorization strategy
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we need to give some context as to what "new" means here - in the future this might not be so clear :)

README.md Outdated
Comment on lines 173 to 174
- [Using Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
- [Configuring access scopes through the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/configuration)
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure how much value these links add here, since they were mentioned above and this is a fairly short section. Fine if you want to keep them, but just wanted to raise this :)


OAuth process by exchanging the current user's [session token (shopify id token)](https://shopify.dev/docs/apps/auth/session-tokens) for an
[access token](https://shopify.dev/docs/apps/auth/access-token-types/online.md) to make
authenticated Shopify API queries. This can replace authorization code grant flow completely if you also take advantage of [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation).
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes it sound like managed install is optional, but that's not the case, right?

authenticated Shopify API queries. This can replace authorization code grant flow completely if you also take advantage of [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation).

To enable token exchange authorization strategy, you can follow the steps in ["New embedded app authorization strategy"](/README.md#new-embedded-app-authorization-strategy).
Upon completion of the token exchange to get the access token, [post authenticated tasks](#post-authenticate-tasks) will be run.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any difference in behaviour as to when post auth tasks are run, when compared to auth code? If so, this might be a good place to call that out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope it'll run the same post auth tasks!

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, but I meant more in the sense of whether there are any scenarios where the auth code flow would trigger the tasks and this won't. I guess there aren't any?

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 don't think so, both will happen right after the app gets a new token. Auth code flow calls it from CallbackController and this is called right after token exchange

with [token exchange](#token-exchange) instead of the authorization code grant flow.


Authorization code grant flow is the previous OAuth flow that requires the app to redirect the user
Copy link
Contributor

Choose a reason for hiding this comment

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

I would try to avoid temporal references here as they will eventually become outdated.

Comment on lines 63 to 65
> [!NOTE]
> If you are building an embedded app, we **strongly** recommend using [Shopify managed installation](https://shopify.dev/docs/apps/auth/installation#shopify-managed-installation)
with [token exchange](#token-exchange) instead of the authorization code grant flow.
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we need the warning here as well if we already have it above, WDYT?

The ShopifyApp gem then redirect the merchant to Shopify, to ask for permission to install the app. (See [ShopifyApp::SessionsController.redirect_to_begin_oauth](https://github.com/Shopify/shopify_app/blob/main/app/controllers/shopify_app/sessions_controller.rb#L76-L96)
for detailed implementation)

### OAuth callback

>️ **Note:** In Shopify App version 8.4.0, we have extracted the callback logic in its own controller. If you are upgrading from a version older than 8.4.0 the callback action and related helper methods were defined in `ShopifyApp::SessionsController` ==> you will have to extend `ShopifyApp::CallbackController` instead and port your logic to the new controller.
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 fairly old warning, I wonder if we still need to have the version reference here, or if we want to make it more "permanent".

docs/shopify_app/sessions.md Show resolved Hide resolved
include ShopifyApp::EnsureHasSession

def index
with_token_refetch(current_shopify_session, shopify_id_token) do
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're using methods provided by EnsureHasSession, do we still need to explicitly pass the session and id token as params? Maybe we could do that internally and keep the params as an override (or not?), that way calling this is a bit simpler.

end
```

If the error is being rescued in the action, it's still possible to make use of `with_token_refetch` provided by `EnsureHasSession` so that a new access token is fetched and the code is executed again with it. This will also update the session parameter with the new attributes.
Copy link
Contributor

Choose a reason for hiding this comment

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

I can see this being a bit confusing for people in the sense that the code might be run twice. Should we explicitly state that only the query should be executed inside the block, and no business logic? Should we have a way to do this in the client itself, something like

def index
  client = Client.new
  client.query({ **options, refetch_id_token: shopify_id_token })
end

I'm mostly just bikeshedding here, wondering if we can go a little bit further to simplify the app's code.

@zzooeeyy zzooeeyy merged commit 44079b0 into main May 2, 2024
6 of 7 checks passed
@zzooeeyy zzooeeyy deleted the add-docs branch May 2, 2024 15:54
@zzooeeyy zzooeeyy mentioned this pull request May 3, 2024
4 tasks
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

3 participants