diff --git a/lib/hooks/login-hooks.js b/lib/hooks/login-hooks.js
new file mode 100644
index 000000000..75df3f90a
--- /dev/null
+++ b/lib/hooks/login-hooks.js
@@ -0,0 +1,121 @@
+// @flow
+
+import * as React from 'react';
+
+import {
+  identityLogInActionTypes,
+  useIdentityPasswordLogIn,
+} from '../actions/user-actions.js';
+import { useKeyserverAuth } from '../keyserver-conn/keyserver-auth.js';
+import { logInActionSources } from '../types/account-types.js';
+import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js';
+import { useDispatchActionPromise } from '../utils/redux-promise-utils.js';
+import { useSelector } from '../utils/redux-utils.js';
+
+// We can't just do everything in one async callback, since the server calls
+// would get bound to Redux state from before the login. In order to pick up the
+// updated CSAT and currentUserInfo from Redux, we break the login into two
+// steps.
+
+type CurrentStep =
+  | { +step: 'inactive' }
+  | {
+      +step: 'identity_login_dispatched',
+      +resolve: () => void,
+      +reject: Error => void,
+    };
+
+const inactiveStep = { step: 'inactive' };
+
+type UsePasswordLogInInput = {
+  // Called after successful identity auth, but before successful authoritative
+  // keyserver auth. Used by callers to trigger local persistence of credentials
+  +saveCredentials?: ?({ +username: string, +password: string }) => mixed,
+};
+function usePasswordLogIn(
+  input?: ?UsePasswordLogInInput,
+): (username: string, password: string) => Promise<void> {
+  const [currentStep, setCurrentStep] =
+    React.useState<CurrentStep>(inactiveStep);
+
+  const saveCredentials = input?.saveCredentials;
+  const identityPasswordLogIn = useIdentityPasswordLogIn();
+  const identityLogInAction = React.useCallback(
+    async (username: string, password: string) => {
+      const result = await identityPasswordLogIn(username, password);
+      saveCredentials?.({ username, password });
+      return result;
+    },
+    [identityPasswordLogIn, saveCredentials],
+  );
+
+  const dispatchActionPromise = useDispatchActionPromise();
+  const returnedFunc = React.useCallback(
+    (username: string, password: string) =>
+      new Promise<void>(
+        // eslint-disable-next-line no-async-promise-executor
+        async (resolve, reject) => {
+          if (currentStep.step !== 'inactive') {
+            return;
+          }
+          const action = identityLogInAction(username, password);
+          void dispatchActionPromise(identityLogInActionTypes, action);
+          try {
+            await action;
+            setCurrentStep({
+              step: 'identity_login_dispatched',
+              resolve,
+              reject,
+            });
+          } catch (e) {
+            reject(e);
+          }
+        },
+      ),
+    [currentStep, dispatchActionPromise, identityLogInAction],
+  );
+
+  const keyserverAuth = useKeyserverAuth(authoritativeKeyserverID());
+
+  const isRegisteredOnIdentity = useSelector(
+    state =>
+      !!state.commServicesAccessToken &&
+      !!state.currentUserInfo &&
+      !state.currentUserInfo.anonymous,
+  );
+
+  const registeringOnAuthoritativeKeyserverRef = React.useRef(false);
+  React.useEffect(() => {
+    if (
+      !isRegisteredOnIdentity ||
+      currentStep.step !== 'identity_login_dispatched' ||
+      registeringOnAuthoritativeKeyserverRef.current
+    ) {
+      return;
+    }
+    registeringOnAuthoritativeKeyserverRef.current = true;
+    const { resolve, reject } = currentStep;
+    void (async () => {
+      try {
+        await keyserverAuth({
+          authActionSource: process.env.BROWSER
+            ? logInActionSources.keyserverAuthFromWeb
+            : logInActionSources.keyserverAuthFromNative,
+          setInProgress: () => {},
+          hasBeenCancelled: () => false,
+          doNotRegister: false,
+        });
+        resolve();
+      } catch (e) {
+        reject(e);
+      } finally {
+        setCurrentStep(inactiveStep);
+        registeringOnAuthoritativeKeyserverRef.current = false;
+      }
+    })();
+  }, [currentStep, isRegisteredOnIdentity, keyserverAuth]);
+
+  return returnedFunc;
+}
+
+export { usePasswordLogIn };
diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js
index d457db8b2..e10433dc9 100644
--- a/native/account/log-in-panel.react.js
+++ b/native/account/log-in-panel.react.js
@@ -1,465 +1,476 @@
 // @flow
 
 import invariant from 'invariant';
 import * as React from 'react';
 import { View, StyleSheet, Keyboard, Platform } from 'react-native';
 import Animated from 'react-native-reanimated';
 
 import {
   legacyLogInActionTypes,
   useLegacyLogIn,
   getOlmSessionInitializationDataActionTypes,
-  useIdentityPasswordLogIn,
-  identityLogInActionTypes,
 } from 'lib/actions/user-actions.js';
+import { usePasswordLogIn } from 'lib/hooks/login-hooks.js';
 import {
   createLoadingStatusSelector,
   combineLoadingStatuses,
 } from 'lib/selectors/loading-selectors.js';
 import {
   validEmailRegex,
   oldValidUsernameRegex,
 } from 'lib/shared/account-utils.js';
 import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js';
 import {
   type LogInInfo,
   type LogInExtraInfo,
   type LogInResult,
   type LogInStartingPayload,
   logInActionSources,
 } from 'lib/types/account-types.js';
-import type { IdentityAuthResult } from 'lib/types/identity-service-types.js';
 import type { LoadingStatus } from 'lib/types/loading-types.js';
 import { getMessageForException } from 'lib/utils/errors.js';
 import {
   useDispatchActionPromise,
   type DispatchActionPromise,
 } from 'lib/utils/redux-promise-utils.js';
 import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js';
 
 import { TextInput } from './modal-components.react.js';
 import {
   fetchNativeCredentials,
   setNativeCredentials,
 } from './native-credentials.js';
 import { PanelButton, Panel } from './panel-components.react.js';
 import PasswordInput from './password-input.react.js';
 import { authoritativeKeyserverID } from '../authoritative-keyserver.js';
 import SWMansionIcon from '../components/swmansion-icon.react.js';
 import { useSelector } from '../redux/redux-utils.js';
 import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js';
 import type { KeyPressEvent } from '../types/react-native.js';
 import {
   AppOutOfDateAlertDetails,
   UnknownErrorAlertDetails,
   UserNotFoundAlertDetails,
 } from '../utils/alert-messages.js';
 import Alert from '../utils/alert.js';
 import type { StateContainer } from '../utils/state-container.js';
 
 export type LogInState = {
   +usernameInputText: ?string,
   +passwordInputText: ?string,
 };
 type BaseProps = {
   +setActiveAlert: (activeAlert: boolean) => void,
   +opacityValue: Animated.Node,
   +logInState: StateContainer<LogInState>,
 };
 type Props = {
   ...BaseProps,
   +loadingStatus: LoadingStatus,
   +logInExtraInfo: () => Promise<LogInExtraInfo>,
   +dispatchActionPromise: DispatchActionPromise,
   +legacyLogIn: (logInInfo: LogInInfo) => Promise<LogInResult>,
-  +identityPasswordLogIn: (
-    username: string,
-    password: string,
-  ) => Promise<IdentityAuthResult>,
+  +identityPasswordLogIn: (username: string, password: string) => Promise<void>,
   +getInitialNotificationsEncryptedMessage: () => Promise<string>,
 };
-class LogInPanel extends React.PureComponent<Props> {
+type State = {
+  +logInPending: boolean,
+};
+class LogInPanel extends React.PureComponent<Props, State> {
   usernameInput: ?TextInput;
   passwordInput: ?PasswordInput;
+  state: State = { logInPending: false };
 
   componentDidMount() {
     void this.attemptToFetchCredentials();
   }
 
   get usernameInputText(): string {
     return this.props.logInState.state.usernameInputText || '';
   }
 
   get passwordInputText(): string {
     return this.props.logInState.state.passwordInputText || '';
   }
 
   async attemptToFetchCredentials() {
     if (
       this.props.logInState.state.usernameInputText !== null &&
       this.props.logInState.state.usernameInputText !== undefined
     ) {
       return;
     }
     const credentials = await fetchNativeCredentials();
     if (!credentials) {
       return;
     }
     if (
       this.props.logInState.state.usernameInputText !== null &&
       this.props.logInState.state.usernameInputText !== undefined
     ) {
       return;
     }
     this.props.logInState.setState({
       usernameInputText: credentials.username,
       passwordInputText: credentials.password,
     });
   }
 
   render(): React.Node {
     return (
       <Panel opacityValue={this.props.opacityValue}>
         <View style={styles.row}>
           <SWMansionIcon
             name="user-1"
             size={22}
             color="#555"
             style={styles.icon}
           />
           <TextInput
             style={styles.input}
             value={this.usernameInputText}
             onChangeText={this.onChangeUsernameInputText}
             onKeyPress={this.onUsernameKeyPress}
             placeholder="Username"
             autoFocus={Platform.OS !== 'ios'}
             autoCorrect={false}
             autoCapitalize="none"
             keyboardType="ascii-capable"
             textContentType="username"
             autoComplete="username"
             returnKeyType="next"
             blurOnSubmit={false}
             onSubmitEditing={this.focusPasswordInput}
-            editable={this.props.loadingStatus !== 'loading'}
+            editable={this.getLoadingStatus() !== 'loading'}
             ref={this.usernameInputRef}
           />
         </View>
         <View style={styles.row}>
           <SWMansionIcon
             name="lock-on"
             size={22}
             color="#555"
             style={styles.icon}
           />
           <PasswordInput
             style={styles.input}
             value={this.passwordInputText}
             onChangeText={this.onChangePasswordInputText}
             placeholder="Password"
             textContentType="password"
             autoComplete="password"
             autoCapitalize="none"
             returnKeyType="go"
             blurOnSubmit={false}
             onSubmitEditing={this.onSubmit}
-            editable={this.props.loadingStatus !== 'loading'}
+            editable={this.getLoadingStatus() !== 'loading'}
             ref={this.passwordInputRef}
           />
         </View>
         <View style={styles.footer}>
           <PanelButton
             text="LOG IN"
-            loadingStatus={this.props.loadingStatus}
+            loadingStatus={this.getLoadingStatus()}
             onSubmit={this.onSubmit}
           />
         </View>
       </Panel>
     );
   }
 
+  getLoadingStatus(): LoadingStatus {
+    if (this.props.loadingStatus === 'loading') {
+      return 'loading';
+    }
+    if (this.state.logInPending) {
+      return 'loading';
+    }
+    return 'inactive';
+  }
+
   usernameInputRef: (usernameInput: ?TextInput) => void = usernameInput => {
     this.usernameInput = usernameInput;
     if (Platform.OS === 'ios' && usernameInput) {
       setTimeout(() => usernameInput.focus());
     }
   };
 
   focusUsernameInput: () => void = () => {
     invariant(this.usernameInput, 'ref should be set');
     this.usernameInput.focus();
   };
 
   passwordInputRef: (passwordInput: ?PasswordInput) => void = passwordInput => {
     this.passwordInput = passwordInput;
   };
 
   focusPasswordInput: () => void = () => {
     invariant(this.passwordInput, 'ref should be set');
     this.passwordInput.focus();
   };
 
   onChangeUsernameInputText: (text: string) => void = text => {
     this.props.logInState.setState({ usernameInputText: text.trim() });
   };
 
   onUsernameKeyPress: (event: KeyPressEvent) => void = event => {
     const { key } = event.nativeEvent;
     if (
       key.length > 1 &&
       key !== 'Backspace' &&
       key !== 'Enter' &&
       this.passwordInputText.length === 0
     ) {
       this.focusPasswordInput();
     }
   };
 
   onChangePasswordInputText: (text: string) => void = text => {
     this.props.logInState.setState({ passwordInputText: text });
   };
 
   onSubmit: () => Promise<void> = async () => {
     this.props.setActiveAlert(true);
     if (this.usernameInputText.search(validEmailRegex) > -1) {
       Alert.alert(
         'Can’t log in with email',
         'You need to log in with your username now',
         [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }],
         { cancelable: false },
       );
       return;
     } else if (this.usernameInputText.search(oldValidUsernameRegex) === -1) {
       Alert.alert(
         'Invalid username',
         'Alphanumeric usernames only',
         [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }],
         { cancelable: false },
       );
       return;
     } else if (this.passwordInputText === '') {
       Alert.alert(
         'Empty password',
         'Password cannot be empty',
         [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }],
         { cancelable: false },
       );
       return;
     }
 
     Keyboard.dismiss();
+    if (usingCommServicesAccessToken) {
+      await this.identityPasswordLogIn();
+      return;
+    }
+
     const extraInfo = await this.props.logInExtraInfo();
     const initialNotificationsEncryptedMessage =
       await this.props.getInitialNotificationsEncryptedMessage();
 
-    if (usingCommServicesAccessToken) {
-      void this.props.dispatchActionPromise(
-        identityLogInActionTypes,
-        this.identityPasswordLogInAction(),
-      );
-    } else {
-      void this.props.dispatchActionPromise(
-        legacyLogInActionTypes,
-        this.legacyLogInAction({
-          ...extraInfo,
-          initialNotificationsEncryptedMessage,
-        }),
-        undefined,
-        ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload),
-      );
-    }
+    void this.props.dispatchActionPromise(
+      legacyLogInActionTypes,
+      this.legacyLogInAction({
+        ...extraInfo,
+        initialNotificationsEncryptedMessage,
+      }),
+      undefined,
+      ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload),
+    );
   };
 
   async legacyLogInAction(extraInfo: LogInExtraInfo): Promise<LogInResult> {
     try {
       const result = await this.props.legacyLogIn({
         ...extraInfo,
         username: this.usernameInputText,
         password: this.passwordInputText,
         authActionSource: logInActionSources.logInFromNativeForm,
       });
       this.props.setActiveAlert(false);
       await setNativeCredentials({
         username: result.currentUserInfo.username,
         password: this.passwordInputText,
       });
       return result;
     } catch (e) {
       if (e.message === 'invalid_credentials') {
         Alert.alert(
           UserNotFoundAlertDetails.title,
           UserNotFoundAlertDetails.message,
           [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }],
           { cancelable: false },
         );
       } else if (e.message === 'client_version_unsupported') {
         Alert.alert(
           AppOutOfDateAlertDetails.title,
           AppOutOfDateAlertDetails.message,
           [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }],
           { cancelable: false },
         );
       } else {
         Alert.alert(
           UnknownErrorAlertDetails.title,
           UnknownErrorAlertDetails.message,
           [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }],
           { cancelable: false },
         );
       }
       throw e;
     }
   }
 
-  async identityPasswordLogInAction(): Promise<IdentityAuthResult> {
+  async identityPasswordLogIn(): Promise<void> {
+    if (this.state.logInPending) {
+      return;
+    }
+    this.setState({ logInPending: true });
     try {
-      const result = await this.props.identityPasswordLogIn(
+      await this.props.identityPasswordLogIn(
         this.usernameInputText,
         this.passwordInputText,
       );
       this.props.setActiveAlert(false);
-      await setNativeCredentials({
-        username: this.usernameInputText,
-        password: this.passwordInputText,
-      });
-      return result;
     } catch (e) {
       const messageForException = getMessageForException(e);
       if (
         messageForException === 'user not found' ||
         messageForException === 'login failed'
       ) {
         Alert.alert(
           UserNotFoundAlertDetails.title,
           UserNotFoundAlertDetails.message,
           [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }],
           { cancelable: false },
         );
       } else if (messageForException === 'Unsupported version') {
         Alert.alert(
           AppOutOfDateAlertDetails.title,
           AppOutOfDateAlertDetails.message,
           [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }],
           { cancelable: false },
         );
       } else {
         Alert.alert(
           UnknownErrorAlertDetails.title,
           UnknownErrorAlertDetails.message,
           [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }],
           { cancelable: false },
         );
       }
       throw e;
+    } finally {
+      this.setState({ logInPending: false });
     }
   }
 
   onUnsuccessfulLoginAlertAckowledged: () => void = () => {
     this.props.setActiveAlert(false);
     this.props.logInState.setState(
       {
         usernameInputText: '',
         passwordInputText: '',
       },
       this.focusUsernameInput,
     );
   };
 
   onUsernameAlertAcknowledged: () => void = () => {
     this.props.setActiveAlert(false);
     this.props.logInState.setState(
       {
         usernameInputText: '',
       },
       this.focusUsernameInput,
     );
   };
 
   onPasswordAlertAcknowledged: () => void = () => {
     this.props.setActiveAlert(false);
     this.props.logInState.setState(
       {
         passwordInputText: '',
       },
       this.focusPasswordInput,
     );
   };
 
   onUnknownErrorAlertAcknowledged: () => void = () => {
     this.props.setActiveAlert(false);
     this.props.logInState.setState(
       {
         usernameInputText: '',
         passwordInputText: '',
       },
       this.focusUsernameInput,
     );
   };
 
   onAppOutOfDateAlertAcknowledged: () => void = () => {
     this.props.setActiveAlert(false);
   };
 }
 
 export type InnerLogInPanel = LogInPanel;
 
 const styles = StyleSheet.create({
   footer: {
     flexDirection: 'row',
     justifyContent: 'flex-end',
   },
   icon: {
     bottom: 10,
     left: 4,
     position: 'absolute',
   },
   input: {
     paddingLeft: 35,
   },
   row: {
     marginHorizontal: 24,
   },
 });
 
 const logInLoadingStatusSelector = createLoadingStatusSelector(
   legacyLogInActionTypes,
 );
 const olmSessionInitializationDataLoadingStatusSelector =
   createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes);
+const identityPasswordLogInInput = { saveCredentials: setNativeCredentials };
 
 const ConnectedLogInPanel: React.ComponentType<BaseProps> =
   React.memo<BaseProps>(function ConnectedLogInPanel(props: BaseProps) {
     const logInLoadingStatus = useSelector(logInLoadingStatusSelector);
     const olmSessionInitializationDataLoadingStatus = useSelector(
       olmSessionInitializationDataLoadingStatusSelector,
     );
     const loadingStatus = combineLoadingStatuses(
       logInLoadingStatus,
       olmSessionInitializationDataLoadingStatus,
     );
 
     const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector);
 
     const dispatchActionPromise = useDispatchActionPromise();
     const callLegacyLogIn = useLegacyLogIn();
-    const callIdentityPasswordLogIn = useIdentityPasswordLogIn();
+    const callIdentityPasswordLogIn = usePasswordLogIn(
+      identityPasswordLogInInput,
+    );
     const getInitialNotificationsEncryptedMessage =
       useInitialNotificationsEncryptedMessage(authoritativeKeyserverID);
 
     return (
       <LogInPanel
         {...props}
         loadingStatus={loadingStatus}
         logInExtraInfo={logInExtraInfo}
         dispatchActionPromise={dispatchActionPromise}
         legacyLogIn={callLegacyLogIn}
         identityPasswordLogIn={callIdentityPasswordLogIn}
         getInitialNotificationsEncryptedMessage={
           getInitialNotificationsEncryptedMessage
         }
       />
     );
   });
 
 export default ConnectedLogInPanel;
diff --git a/web/account/traditional-login-form.react.js b/web/account/traditional-login-form.react.js
index b7e4aab83..209e48268 100644
--- a/web/account/traditional-login-form.react.js
+++ b/web/account/traditional-login-form.react.js
@@ -1,235 +1,247 @@
 // @flow
 
 import invariant from 'invariant';
 import * as React from 'react';
 
 import {
   useLegacyLogIn,
   legacyLogInActionTypes,
-  useIdentityPasswordLogIn,
-  identityLogInActionTypes,
 } from 'lib/actions/user-actions.js';
 import { useModalContext } from 'lib/components/modal-provider.react.js';
+import { usePasswordLogIn } from 'lib/hooks/login-hooks.js';
 import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js';
 import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
 import {
   oldValidUsernameRegex,
   validEmailRegex,
 } from 'lib/shared/account-utils.js';
 import type {
   LogInExtraInfo,
   LogInStartingPayload,
 } from 'lib/types/account-types.js';
 import { logInActionSources } from 'lib/types/account-types.js';
 import { getMessageForException } from 'lib/utils/errors.js';
 import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js';
 import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js';
 
 import HeaderSeparator from './header-separator.react.js';
 import css from './log-in-form.css';
 import PasswordInput from './password-input.react.js';
 import Button from '../components/button.react.js';
 import { olmAPI } from '../crypto/olm-api.js';
 import LoadingIndicator from '../loading-indicator.react.js';
 import Input from '../modals/input.react.js';
 import { useSelector } from '../redux/redux-utils.js';
 
 const loadingStatusSelector = createLoadingStatusSelector(
   legacyLogInActionTypes,
 );
 function TraditionalLoginForm(): React.Node {
-  const inputDisabled = useSelector(loadingStatusSelector) === 'loading';
+  const legacyAuthInProgress = useSelector(loadingStatusSelector) === 'loading';
+  const [identityAuthInProgress, setIdentityAuthInProgress] =
+    React.useState(false);
+  const inputDisabled = legacyAuthInProgress || identityAuthInProgress;
+
   const loginExtraInfo = useSelector(logInExtraInfoSelector);
   const callLegacyLogIn = useLegacyLogIn();
-  const callIdentityPasswordLogIn = useIdentityPasswordLogIn();
 
   const dispatchActionPromise = useDispatchActionPromise();
   const modalContext = useModalContext();
 
   const usernameInputRef = React.useRef<?HTMLInputElement>();
   React.useEffect(() => {
     usernameInputRef.current?.focus();
   }, []);
 
   const [username, setUsername] = React.useState<string>('');
   const onUsernameChange = React.useCallback(
     (e: SyntheticEvent<HTMLInputElement>) => {
       invariant(e.target instanceof HTMLInputElement, 'target not input');
       setUsername(e.target.value);
     },
     [],
   );
 
   const onUsernameBlur = React.useCallback(() => {
     setUsername(untrimmedUsername => untrimmedUsername.trim());
   }, []);
 
   const [password, setPassword] = React.useState<string>('');
   const onPasswordChange = React.useCallback(
     (e: SyntheticEvent<HTMLInputElement>) => {
       invariant(e.target instanceof HTMLInputElement, 'target not input');
       setPassword(e.target.value);
     },
     [],
   );
 
   const [errorMessage, setErrorMessage] = React.useState<string>('');
 
   const legacyLogInAction = React.useCallback(
     async (extraInfo: LogInExtraInfo) => {
       await olmAPI.initializeCryptoAccount();
       const userPublicKey = await olmAPI.getUserPublicKey();
       try {
         const result = await callLegacyLogIn({
           ...extraInfo,
           username,
           password,
           authActionSource: logInActionSources.logInFromWebForm,
           signedIdentityKeysBlob: {
             payload: userPublicKey.blobPayload,
             signature: userPublicKey.signature,
           },
         });
         modalContext.popModal();
         return result;
       } catch (e) {
         setUsername('');
         setPassword('');
         if (getMessageForException(e) === 'invalid_credentials') {
           setErrorMessage('incorrect username or password');
         } else {
           setErrorMessage('unknown error');
         }
         usernameInputRef.current?.focus();
         throw e;
       }
     },
     [callLegacyLogIn, modalContext, password, username],
   );
 
+  const callIdentityPasswordLogIn = usePasswordLogIn();
+
   const identityPasswordLogInAction = React.useCallback(async () => {
+    if (identityAuthInProgress) {
+      return;
+    }
+    setIdentityAuthInProgress(true);
     try {
-      const result = await callIdentityPasswordLogIn(username, password);
+      await callIdentityPasswordLogIn(username, password);
       modalContext.popModal();
-      return result;
     } catch (e) {
       setUsername('');
       setPassword('');
       const messageForException = getMessageForException(e);
       if (
         messageForException === 'user not found' ||
         messageForException === 'login failed'
       ) {
         setErrorMessage('incorrect username or password');
       } else {
         setErrorMessage('unknown error');
       }
       usernameInputRef.current?.focus();
       throw e;
+    } finally {
+      setIdentityAuthInProgress(false);
     }
-  }, [callIdentityPasswordLogIn, modalContext, password, username]);
+  }, [
+    identityAuthInProgress,
+    callIdentityPasswordLogIn,
+    modalContext,
+    password,
+    username,
+  ]);
 
   const onSubmit = React.useCallback(
     (event: SyntheticEvent<HTMLElement>) => {
       event.preventDefault();
 
       if (username.search(validEmailRegex) > -1) {
         setUsername('');
         setErrorMessage('usernames only, not emails');
         usernameInputRef.current?.focus();
         return;
       } else if (username.search(oldValidUsernameRegex) === -1) {
         setUsername('');
         setErrorMessage('alphanumeric usernames only');
         usernameInputRef.current?.focus();
         return;
       } else if (password === '') {
         setErrorMessage('password is empty');
         usernameInputRef.current?.focus();
         return;
       }
       if (usingCommServicesAccessToken) {
-        void dispatchActionPromise(
-          identityLogInActionTypes,
-          identityPasswordLogInAction(),
-        );
+        void identityPasswordLogInAction();
       } else {
         void dispatchActionPromise(
           legacyLogInActionTypes,
           legacyLogInAction(loginExtraInfo),
           undefined,
           ({
             calendarQuery: loginExtraInfo.calendarQuery,
           }: LogInStartingPayload),
         );
       }
     },
     [
       dispatchActionPromise,
       identityPasswordLogInAction,
       legacyLogInAction,
       loginExtraInfo,
       username,
       password,
     ],
   );
 
   const loginButtonContent = React.useMemo(() => {
     if (inputDisabled) {
       return <LoadingIndicator status="loading" />;
     }
     return 'Sign in';
   }, [inputDisabled]);
 
   const signInButtonColor = React.useMemo(
     () => ({ backgroundColor: '#6A20E3' }),
     [],
   );
 
   return (
     <form method="POST">
       <div>
         <h4>Sign in to Comm</h4>
         <HeaderSeparator />
         <div className={css.form_title}>Username</div>
         <div className={css.form_content}>
           <Input
             className={css.input}
             type="text"
             placeholder="Username"
             value={username}
             onChange={onUsernameChange}
             onBlur={onUsernameBlur}
             ref={usernameInputRef}
             disabled={inputDisabled}
           />
         </div>
       </div>
       <div>
         <div className={css.form_title}>Password</div>
         <div className={css.form_content}>
           <PasswordInput
             className={css.input}
             value={password}
             onChange={onPasswordChange}
             disabled={inputDisabled}
           />
         </div>
       </div>
       <div className={css.form_footer}>
         <Button
           variant="filled"
           type="submit"
           disabled={inputDisabled}
           onClick={onSubmit}
           buttonColor={signInButtonColor}
         >
           {loginButtonContent}
         </Button>
         <div className={css.modal_form_error}>{errorMessage}</div>
       </div>
     </form>
   );
 }
 
 export default TraditionalLoginForm;