Fix auth refresh in certain edge cases (#10218)

This PR should fix a bug when session doesn't refresh when a computer comes back from sleep mode
This commit is contained in:
Sergei Garin 2024-06-10 20:21:36 +03:00 committed by GitHub
parent d7689b3357
commit f12e985b3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 56 deletions

View File

@ -21,10 +21,14 @@ export default function StatelessSpinner(props: StatelessSpinnerProps) {
const [state, setState] = React.useState(spinner.SpinnerState.initial) const [state, setState] = React.useState(spinner.SpinnerState.initial)
React.useEffect(() => { React.useEffect(() => {
window.setTimeout(() => { const timeout = window.setTimeout(() => {
setState(rawState) setState(rawState)
}) })
}, [/* should never change */ rawState])
return () => {
window.clearTimeout(timeout)
}
}, [rawState])
return <Spinner state={state} {...(size != null ? { size } : {})} /> return <Spinner state={state} {...(size != null ? { size } : {})} />
} }

View File

@ -171,7 +171,7 @@ export default function AuthProvider(props: AuthProviderProps) {
const { children, projectManagerUrl, projectManagerRootDirectory } = props const { children, projectManagerUrl, projectManagerRootDirectory } = props
const logger = loggerProvider.useLogger() const logger = loggerProvider.useLogger()
const { cognito } = authService ?? {} const { cognito } = authService ?? {}
const { session, deinitializeSession, onSessionError } = sessionProvider.useSession() const { session, onSessionError } = sessionProvider.useSession()
const { setBackendWithoutSavingType } = backendProvider.useStrictSetBackend() const { setBackendWithoutSavingType } = backendProvider.useStrictSetBackend()
const { localStorage } = localStorageProvider.useLocalStorage() const { localStorage } = localStorageProvider.useLocalStorage()
const { getText } = textProvider.useText() const { getText } = textProvider.useText()
@ -628,7 +628,6 @@ export default function AuthProvider(props: AuthProviderProps) {
gtagEvent('cloud_sign_out') gtagEvent('cloud_sign_out')
cognito.saveAccessToken(null) cognito.saveAccessToken(null)
localStorage.clearUserSpecificEntries() localStorage.clearUserSpecificEntries()
deinitializeSession()
setInitialized(false) setInitialized(false)
sentry.setUser(null) sentry.setUser(null)
setUserSession(null) setUserSession(null)

View File

@ -4,9 +4,6 @@ import * as React from 'react'
import * as reactQuery from '@tanstack/react-query' import * as reactQuery from '@tanstack/react-query'
import * as asyncEffectHooks from '#/hooks/asyncEffectHooks'
import * as refreshHooks from '#/hooks/refreshHooks'
import * as errorModule from '#/utilities/error' import * as errorModule from '#/utilities/error'
import type * as cognito from '#/authentication/cognito' import type * as cognito from '#/authentication/cognito'
@ -19,8 +16,6 @@ import * as listen from '#/authentication/listen'
/** State contained in a {@link SessionContext}. */ /** State contained in a {@link SessionContext}. */
interface SessionContextType { interface SessionContextType {
readonly session: cognito.UserSession | null readonly session: cognito.UserSession | null
/** Set `initialized` to false. Must be called when logging out. */
readonly deinitializeSession: () => void
readonly onSessionError: (callback: (error: Error) => void) => () => void readonly onSessionError: (callback: (error: Error) => void) => () => void
} }
@ -51,14 +46,14 @@ export interface SessionProviderProps {
} }
const FIVE_MINUTES_MS = 300_000 const FIVE_MINUTES_MS = 300_000
// const SIX_HOURS_MS = 21_600_000
const SIX_HOURS_MS = 21_600_000 const SIX_HOURS_MS = 21_600_000
/** A React provider for the session of the authenticated user. */ /** A React provider for the session of the authenticated user. */
export default function SessionProvider(props: SessionProviderProps) { export default function SessionProvider(props: SessionProviderProps) {
const { mainPageUrl, children, userSession, registerAuthEventListener, refreshUserSession } = const { mainPageUrl, children, userSession, registerAuthEventListener, refreshUserSession } =
props props
const [refresh, doRefresh] = refreshHooks.useRefresh()
const [initialized, setInitialized] = React.useState(false)
const errorCallbacks = React.useRef(new Set<(error: Error) => void>()) const errorCallbacks = React.useRef(new Set<(error: Error) => void>())
/** Returns a function to unregister the listener. */ /** Returns a function to unregister the listener. */
@ -69,47 +64,40 @@ export default function SessionProvider(props: SessionProviderProps) {
} }
}, []) }, [])
// Register an async effect that will fetch the user's session whenever the `refresh` state is const queryClient = reactQuery.useQueryClient()
// set. This is useful when a user has just logged in (as their cached credentials are
// out of date, so this will update them).
const session = asyncEffectHooks.useAsyncEffect(
null,
async () => {
if (userSession == null) {
setInitialized(true)
return null
} else {
try {
const innerSession = await userSession()
setInitialized(true)
return innerSession
} catch (error) {
if (error instanceof Error) {
for (const listener of errorCallbacks.current) {
listener(error)
}
}
throw error
}
}
},
[refresh]
)
const timeUntilRefresh = session const session = reactQuery.useSuspenseQuery({
queryKey: ['userSession', userSession],
queryFn: userSession
? () =>
userSession().catch(error => {
if (error instanceof Error) {
for (const listener of errorCallbacks.current) {
listener(error)
}
}
throw error
})
: reactQuery.skipToken,
refetchOnWindowFocus: true,
refetchIntervalInBackground: true,
})
const timeUntilRefresh = session.data
? // If the session has not expired, we should refresh it when it is 5 minutes from expiring. ? // If the session has not expired, we should refresh it when it is 5 minutes from expiring.
new Date(session.expireAt).getTime() - Date.now() - FIVE_MINUTES_MS new Date(session.data.expireAt).getTime() - Date.now() - FIVE_MINUTES_MS
: Infinity : Infinity
const refreshUserSessionMutation = reactQuery.useMutation({
mutationKey: ['refreshUserSession', session.data],
mutationFn: () => refreshUserSession?.().then(() => null) ?? Promise.resolve(),
meta: { invalidates: [['userSession']], awaitInvalidates: true },
})
reactQuery.useQuery({ reactQuery.useQuery({
queryKey: ['userSession'], queryKey: ['refreshUserSession'],
queryFn: refreshUserSession queryFn: refreshUserSession
? () => ? () => refreshUserSessionMutation.mutateAsync()
refreshUserSession()
.then(() => {
doRefresh()
})
.then(() => null)
: reactQuery.skipToken, : reactQuery.skipToken,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchIntervalInBackground: true, refetchIntervalInBackground: true,
@ -119,7 +107,6 @@ export default function SessionProvider(props: SessionProviderProps) {
// Register an effect that will listen for authentication events. When the event occurs, we // Register an effect that will listen for authentication events. When the event occurs, we
// will refresh or clear the user's session, forcing a re-render of the page with the new // will refresh or clear the user's session, forcing a re-render of the page with the new
// session. // session.
//
// For example, if a user clicks the "sign out" button, this will clear the user's session, which // For example, if a user clicks the "sign out" button, this will clear the user's session, which
// means the login screen (which is a child of this provider) should render. // means the login screen (which is a child of this provider) should render.
React.useEffect( React.useEffect(
@ -128,7 +115,7 @@ export default function SessionProvider(props: SessionProviderProps) {
switch (event) { switch (event) {
case listen.AuthEvent.signIn: case listen.AuthEvent.signIn:
case listen.AuthEvent.signOut: { case listen.AuthEvent.signOut: {
doRefresh() void queryClient.invalidateQueries({ queryKey: ['userSession'] })
break break
} }
case listen.AuthEvent.customOAuthState: case listen.AuthEvent.customOAuthState:
@ -139,7 +126,7 @@ export default function SessionProvider(props: SessionProviderProps) {
// will not work. // will not work.
// See https://github.com/aws-amplify/amplify-js/issues/3391#issuecomment-756473970 // See https://github.com/aws-amplify/amplify-js/issues/3391#issuecomment-756473970
history.replaceState({}, '', mainPageUrl) history.replaceState({}, '', mainPageUrl)
doRefresh() void queryClient.invalidateQueries({ queryKey: ['userSession'] })
break break
} }
default: { default: {
@ -147,16 +134,12 @@ export default function SessionProvider(props: SessionProviderProps) {
} }
} }
}), }),
[doRefresh, registerAuthEventListener, mainPageUrl] [registerAuthEventListener, mainPageUrl, queryClient]
) )
const deinitializeSession = () => {
setInitialized(false)
}
return ( return (
<SessionContext.Provider value={{ session, deinitializeSession, onSessionError }}> <SessionContext.Provider value={{ session: session.data, onSessionError }}>
{initialized && children} {children}
</SessionContext.Provider> </SessionContext.Provider>
) )
} }