diff --git a/lib/keyserver-conn/recovery-utils.js b/lib/keyserver-conn/recovery-utils.js index 287b7324d..0dc3a612e 100644 --- a/lib/keyserver-conn/recovery-utils.js +++ b/lib/keyserver-conn/recovery-utils.js @@ -1,263 +1,269 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { setNewSession, type CallKeyserverEndpoint, } from './keyserver-conn-types.js'; import { logOutActionTypes, useLogOut } from '../actions/user-actions.js'; import { cookieSelector, sessionIDSelector, urlPrefixSelector, } from '../selectors/keyserver-selectors.js'; import type { RecoveryActionSource } from '../types/account-types.js'; import type { Endpoint } from '../types/endpoints.js'; import type { Dispatch } from '../types/redux-types.js'; import { type ClientSessionChange, genericCookieInvalidation, type PreRequestUserState, } from '../types/session-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import callSingleKeyServerEndpoint from '../utils/call-single-keyserver-endpoint.js'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../utils/call-single-keyserver-endpoint.js'; import { getConfig } from '../utils/config.js'; import { promiseAll } from '../utils/promises.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken, relyingOnAuthoritativeKeyserver, } from '../utils/services-utils.js'; // This function is a shortcut that tells us whether it's worth even trying to // call resolveKeyserverSessionInvalidation function canResolveKeyserverSessionInvalidation(): boolean { if (usingCommServicesAccessToken) { // We can always try to resolve a keyserver session invalidation // automatically using the Olm auth responder return true; } const { resolveKeyserverSessionInvalidationUsingNativeCredentials } = getConfig(); // If we can't use the Olm auth responder, then we can only resolve a // keyserver session invalidation on native, where we have access to the // user's native credentials. Note that we can't do this for ETH users, but we // don't know if the user is an ETH user from this function return !!resolveKeyserverSessionInvalidationUsingNativeCredentials; } // This function attempts to resolve an invalid keyserver session. A session can // become invalid when a keyserver invalidates it, or due to inconsistent client // state. If the client is usingCommServicesAccessToken, then the invalidation // recovery will try to go through the keyserver's Olm auth responder. // Otherwise, it will attempt to use the user's credentials to log in with the // legacy auth responder, which won't work on web and won't work for ETH users. async function resolveKeyserverSessionInvalidation( dispatch: Dispatch, cookie: ?string, urlPrefix: string, recoveryActionSource: RecoveryActionSource, keyserverID: string, actionFunc: ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, ) => Promise, ): Promise { let newSessionChange = null; const boundCallSingleKeyserverEndpoint = async ( endpoint: Endpoint, data: { +[key: string]: mixed }, options?: ?CallSingleKeyserverEndpointOptions, ) => { const innerBoundSetNewSession = ( sessionChange: ClientSessionChange, error: ?string, ) => { newSessionChange = sessionChange; setNewSession( dispatch, sessionChange, null, error, recoveryActionSource, keyserverID, ); }; return await callSingleKeyServerEndpoint( cookie, innerBoundSetNewSession, () => new Promise(r => r(null)), () => new Promise(r => r(null)), urlPrefix, null, false, null, null, endpoint, data, dispatch, options, false, keyserverID, ); }; const boundCallKeyserverEndpoint = ( endpoint: Endpoint, requests: { +[keyserverID: string]: ?{ +[string]: mixed } }, options?: ?CallSingleKeyserverEndpointOptions, ) => { if (requests[keyserverID]) { const promises = { [keyserverID]: boundCallSingleKeyserverEndpoint( endpoint, requests[keyserverID], options, ), }; return promiseAll(promises); } return Promise.resolve({}); }; await actionFunc( boundCallSingleKeyserverEndpoint, boundCallKeyserverEndpoint, ); return newSessionChange; } function useKeyserverRecoveryLogIn( keyserverID: string, ): ( source: RecoveryActionSource, actionFunc: ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, ) => Promise, hasBeenCancelled: () => boolean, ) => Promise { const preRequestUserInfo = useSelector(state => state.currentUserInfo); const cookie = useSelector(cookieSelector(keyserverID)); const sessionID = useSelector(sessionIDSelector(keyserverID)); const preRequestUserState = React.useMemo( () => ({ currentUserInfo: preRequestUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID, }, }, }), [preRequestUserInfo, keyserverID, cookie, sessionID], ); // We only need to do a "spot check" on this value below. // - To avoid regenerating performRecovery whenever it changes, we want to // make sure it's not in that function's dep list. // - If we exclude it from that function's dep list, we'll end up binding in // the value of preRequestUserState at the time performRecovery is updated. // Instead, by assigning to a ref, we are able to use the latest value. const preRequestUserStateRef = React.useRef(preRequestUserState); preRequestUserStateRef.current = preRequestUserState; const dispatch = useDispatch(); const urlPrefix = useSelector(urlPrefixSelector(keyserverID)); const logOut = useLogOut(); const dispatchActionPromise = useDispatchActionPromise(); const invalidateKeyserverSession = React.useCallback( ( source: RecoveryActionSource, sessionChange: ClientSessionChange, userStateBeforeRecovery: PreRequestUserState, + hasBeenCancelled: () => boolean, ) => { + if (hasBeenCancelled()) { + return; + } setNewSession( dispatch, sessionChange, userStateBeforeRecovery, null, source, keyserverID, ); if ( keyserverID === authoritativeKeyserverID() && relyingOnAuthoritativeKeyserver ) { void dispatchActionPromise(logOutActionTypes, logOut()); } }, [dispatch, keyserverID, dispatchActionPromise, logOut], ); return React.useCallback( async ( source: RecoveryActionSource, actionFunc: ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, ) => Promise, hasBeenCancelled: () => boolean, ) => { invariant( urlPrefix, `urlPrefix for ${keyserverID} should be set during recovery login`, ); const userStateBeforeRecovery = preRequestUserStateRef.current; try { const recoverySessionChange = await resolveKeyserverSessionInvalidation( dispatch, cookie, urlPrefix, source, keyserverID, actionFunc, ); const sessionChange = recoverySessionChange ?? genericCookieInvalidation; if ( sessionChange.cookieInvalidated || !sessionChange.cookie || !sessionChange.cookie.startsWith('user=') ) { invalidateKeyserverSession( source, sessionChange, userStateBeforeRecovery, + hasBeenCancelled, ); } } catch (e) { if (hasBeenCancelled()) { return; } console.log( `Error during recovery login with keyserver ${keyserverID}`, e, ); invalidateKeyserverSession( source, genericCookieInvalidation, userStateBeforeRecovery, + hasBeenCancelled, ); throw e; } }, [keyserverID, dispatch, cookie, urlPrefix, invalidateKeyserverSession], ); } export { canResolveKeyserverSessionInvalidation, resolveKeyserverSessionInvalidation, useKeyserverRecoveryLogIn, };