-
Notifications
You must be signed in to change notification settings - Fork 685
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 middleware to schedule sign-out #2904
Conversation
|
const CLEAR_TOKEN = userActions.clearToken.toString(); | ||
const GET_DETAILS = userActions.getDetails.request.toString(); | ||
|
||
const isSigningIn = type => type === SET_TOKEN || type === GET_DETAILS; |
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 || GET_DETAILS
? Is this a safeguard against tokens invalidating between the token set and the details fetch?
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 a user starts up the application, if they have a valid token in storage, we sign them in. Since they already have a token, a SET_TOKEN
action is never dispatched. Instead, the first user action is GET_DETAILS
.
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.
So it's really "is signing in or is getting user details". And in both cases we set a timer based on the time remaining on the TTL. Got it.
const scheduleSignOut = store => next => action => { | ||
const { dispatch } = store; | ||
|
||
if (isSigningIn(action.type)) { |
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.
So when we sign in, we create a timer to remove the token based on the TTL of the token just set? So this is similar to what getItem does but is also able to dispatch the signOut
action and clear the token from redux (persisted as M2_VENIA_BROWSER_PERSISTENCE__signin_token
).
The current iteration of the auth link also uses the getItem
mechanism which should delete the token if expired.
Both seem to handle similar situations related to client-side invalidation of the token, but I still don't know if we gracefully handle a user token that invalidates server-side before the client-side expiry.
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.
Correct, getItem
can't dispatch signOut
, so the Redux store contains stale state (isSignedIn: true
) when there's an expired token or no token in storage.
This PR doesn't handle a server-side invalidation, but I believe those are going to be much less frequent. Scheduled expiration should be the 99% case.
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 PR doesn't handle a server-side invalidation, but I believe those are going to be much less frequent. Scheduled expiration should be the 99% case.
Right, Tommy's solution gets us there, assuming the error messages are correct. That said, I don't know enough about auth invalidation so I'll trust that you when you say that a timeout expiry is the most common scenario.
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 need to solve for the cache clearing just like we do in the other sign out handlers. :(
The |
cartApi.getCartDetails({ | ||
apolloClient, |
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.
Passing things that can't be serialized, such as functions or objects containing functions, in action payloads is an anti-pattern in Redux. I understand the thought behind putting apolloClient
in the payload, but we shouldn't have been doing this.
|
||
return async function thunk(dispatch, getState) { | ||
return async function thunk(dispatch, getState, { apolloClient }) { |
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.
Now that our thunk middleware includes an extraArgument
object with the Apollo client, we can access it from any thunk.
@@ -23,6 +25,8 @@ export const signOut = (payload = {}) => | |||
await dispatch(clearToken()); | |||
await dispatch(actions.reset()); | |||
await clearCheckoutDataFromStorage(); | |||
await clearCartDataFromCache(apolloClient); | |||
await clearCustomerDataFromCache(apolloClient); |
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.
Here, we clear the rest of the cache data.
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 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.
It looks like maybe we have to add it to the signIn
async action as well in order to remove it from other handlers.
import thunk from 'redux-thunk'; | ||
|
||
export const extraArgument = {}; | ||
export default thunk.withExtraArgument(extraArgument); |
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.
Fortunately for us, redux-thunk
has already considered our exact situation and provides a method that makes it easy to provide our API client to every thunk.
apolloClient = apollo.client; | ||
} else { | ||
apolloClient = new ApolloClient({ | ||
const apolloClient = useMemo(() => { |
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 should always have been memoized. Performing side effects in the body of a render function (component) has always been an anti-pattern; here, the original code was creating multiple Apollo clients as Adapter
is executed several times during initial rendering.
An easy mistake to make, but a very important one to avoid.
await apolloClient.persistor.restore(); | ||
|
||
// attach the Apollo client to the Redux store | ||
attachClient(apolloClient); |
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 the only real change to this file. Ideally, we would create the Apollo client at the same time we create the Redux store and provide it directly to the middleware, but since we've delegated client creation to Adapter
, we have to wait and add it here.
// Clear the cart data from apollo client if we get here and | ||
// have an apolloClient. | ||
if (apolloClient) { | ||
await clearCartDataFromCache(apolloClient); |
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 did we remove this?
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.
Excellent work on this solution. And nice find on the adapter bug.
I do have concerns about alerting the user that they are being signed out, but I believe we are going to handle that detail in another ticket. The main point of this effort was to not leave the user in a stuck state where their token was invalid but not cleared.
@jimbo Pls check 2 observations -
|
@jimbo Looks like issue is happening on desktop browser as well, if you leave idle for an hour then auto sign-out does not happen unless new call is made. |
QA Approved. |
Description
When a user signs in, we get a token that we store in both app state (Redux store's
user
slice) and local storage (signin_token
, namespaced). This token has a limited lifetime before it becomes invalid (currently hardcoded to one hour), and an invalid token is removed from storage the first time it is accessed after expiration.Unfortunately, the storage interface can't notify app state when the token expires or when it removes an expired token, since it doesn't have access to the
dispatch
function. This leads app state and storage to fall out of sync, such that components start to throw errors when app state tells them the user is signed in but the token has actually been invalidated.This PR adds a middleware that schedules the app state's
signOut
workflow to run whenever a token is set (sign-in or re-auth). Just as the token expires, the app will sign the user out, clear the token and all user state, and refresh the page. In the future, we can remove the page refresh, but that will require some amount of component rework and regression.Related Issue
PWA-1105
Acceptance
Verification Stakeholders
Specification
Verification Steps
signin_token
in storage to have attl
of30000
(30 seconds).Screenshots / Screen Captures (if appropriate)
Checklist