-
Notifications
You must be signed in to change notification settings - Fork 125
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
Token exchange prototype #448
Conversation
|
||
logger.debug('Requesting token exchange'); | ||
|
||
const {session} = await api.auth.tokenExchange({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an access token is expired or does not exist, we will perform token exchange to get a new access token.
if (persistedSession.isScopeChanged(config.scopes)) { | ||
config.sessionStorage.deleteSession(persistedSession.id); | ||
|
||
this.redirectToInstall(request, shop); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an edge case that can happen when the app changes scopes, while a merchant has an active app browser session. Please see the discussion for more details.
const {api, logger, config} = this; | ||
|
||
const isXhrRequest = request.headers.get('authorization'); | ||
if (!isXhrRequest) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the request is a document load with an invalid or non-existent session token, we will redirect to the bounce page to render App Bridge and redirect to the original page with the session token passed in the authorization header
return redirectToBouncePage(new URL(request.url), {api, logger, config}); | ||
} | ||
|
||
return new Response(undefined, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For an XHR request made via App Bridge's augmented fetch, return a 401
with the X-Shopify-Invalid-Session
header.
|
||
console.log('error.response', JSON.stringify(error.response)); | ||
|
||
if (error.response.code === 401) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When making Admin API requests, a 401
signifies that the access token is invalid.
params.config.sessionStorage.deleteSession(session.id); | ||
|
||
const isXhrRequest = request.headers.get('authorization'); | ||
if (isXhrRequest) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this scenario, we want to perform token exchange. So we delete the access token and return a 401
with X-Shopify-Invalid-Session
so that the augmented fetch can retry the request, consequently triggering token exchange.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rachel-carvalho I'm starting to think we shouldn't retry ever since it basically retries the entire request to the remix server:
loader() {
const {admin} = authenticate.admin(request)
add_some_records_to_local_db();
non_shopify_api_calls();
admin.createProducts() <- this is what we want to retry but would end up re-running all the previous calls.
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might be worth simply redirecting to /session-token?url={request.url}
to allow the app to naturally perform token exchange without ever retrying the request.
}, | ||
}); | ||
} else { | ||
throw redirect(request.url); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an Admin API request is happening on a document load, we want to delete the access token and refresh the page in order to trigger token exchange.
} else if (isAuthRequest) { | ||
const shop = ensureValidShopParam(request, {api, logger, config}); | ||
const installUrl = `https://${shop}/admin/oauth/install?client_id=${config.apiKey}`; | ||
throw redirect(installUrl); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to handle auth requests in token exchange?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is related to Option 1 of handling access scopes changes during a merchant's app browser session.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're still handling the /auth
route so that the login page where the developer can type their store url to initiate install could still work, since that's a nice tool during app development.
Then instead of initiating auth code flow like the original authenticate
strategy does, it just redirects the user to the shopify managed install page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the context 👍
const sessionToken = await validateSessionTokenUncaught( | ||
{api, logger, config}, | ||
sessionTokenString, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How come we need the Uncaught version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We catch the invalid JWT error and either return response header X-Shopify-Invalid-Session
for XHR requests or redirect to bounce page. We could probably do this better but keeping this rough because we're in prototype phase.
logger.info('Authenticating admin request'); | ||
|
||
if (sessionTokenParam) { | ||
await validateUrlParams(request, {api, logger, config}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is validing the URL params overkill given?
- I think we can get all the same data from the session token
- The session token can only be constructed using the API secret.
- The URL params also use the API secret.
Worth checking with app sec? If so, we get to simplify our token exchange implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is the ability on the browser to simply refresh the iFrame instead of the entire page. So after a minute if the merchant only reloads the iFrame, it'll use an expired token. We should avoid that as that same session token might be used to make a token exchange request (which fails).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good catch.
But why does that mean we need to validate the URL params and the session token? Why not just the session token, the URL params expire in roughly the same timeframe?
|
||
if (sessionTokenParam) { | ||
await validateUrlParams(request, {api, logger, config}); | ||
await ensureAppIsEmbeddedIfRequired(request, {api, logger, config}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is interesting.
In auth code flow ensureAppIsEmbeddedIfRequired
makes sense. In token exchange IfRequired
makes less sense, because it's always required?
When we product-ionize this code these are the kind of helpers that I'd want to be careful about. I'd prefer to duplicate the helper and reduce coupling between authe code flow and token exchange
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Completely agree, we want the new embedded strategy to be independent from the existing auth code flow.
We wanted to only include net new token exchange code in embedded-authenticate
so that the diff here in the draft PR was easier to review, and we didn't want to go too much into refactoring before build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes complete sense, thanks Rachel.
admin: oauth.authenticateAdmin.bind(oauth), | ||
admin: config.isEmbeddedApp | ||
? tokenExchange.authenticateAdmin.bind(tokenExchange) | ||
: oauth.authenticateAdmin.bind(oauth), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this ❤️
5bdfbe3
to
04439db
Compare
…valid on an XHR request
04439db
to
333d14b
Compare
333d14b
to
2398cbf
Compare
WHY are these changes introduced?
Fixes #0000
WHAT is this pull request doing?
Type of change
Checklist
yarn changeset
to create a draft changelog entry (do NOT update theCHANGELOG.md
files manually)