Auto-logout user after inactivity - React Native

mobile-security.png

Some mobile applications, like financial apps have sensitive data in them. Leaving such data exposed even when the users are not active pose a security flaw as hackers can gain access to the phone and steal the sensitive data. To mitigate this, it is good practice to logout the user after detecting inactivity on the app for some time (varies from app to app). In this article, I will be sharing how I achieved this on a project I worked on.

The plan for me was to detect the AppState changes and logout the user when the app is in the background state. This has limitation in itself (will be discussed in a later article). After more digging, I settled on using PanResponder which detects touch and reconciles it into a single gesture.

Let's get started

In your application, create a custom hook, I call mine useAutoLogout.ts(use whatever name you like). The file should have the code below

import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { PanResponder } from 'react-native';
import { useNavigation } from '@react-navigation/native';

export default () => {
  const dispatch = useDispatch();
  const navigation = useNavigation();
  const token = "token value";
  const lastInteraction = useRef(new Date());
  const [timeWentInactive, setTimeWentInactive] = useState<Date | null>(null);
  const inactivityTimer = useRef<boolean | NodeJS.Timeout | number>(false);
  const waitForInactivity = useRef<number>(0);

  const INACTIVITY_CHECK_INTERVAL_MS = 1000;

  useEffect(() => {
    if (token) {
      //  180 secs
      const autologoutTime = 180;
      waitForInactivity.current = autologoutTime * 1000;
    }
  }, [token, waitForInactivity.current]);

  const performAutoLogout = useCallback(() => {
    navigation.navigate('Logout');
  }, []);

  const checkInactive = useCallback(() => {
    if (inactivityTimer.current) {
      return;
    }
    inactivityTimer.current = setInterval(() => {
      if (Math.abs(new Date().valueOf() - lastInteraction.current.valueOf()) >=
        waitForInactivity.current) {
        setIsInactive();
      }
    }, INACTIVITY_CHECK_INTERVAL_MS);
  }, []);

  useEffect(() => {
    if (token) {
      checkInactive();
    }
  }, [checkInactive]);

  const setIsActive = useCallback(() => {
    lastInteraction.current = new Date();
    if (timeWentInactive) {
      setTimeWentInactive(null);
    }

    if (token) {
      checkInactive();
    }
  }, []);

  const setIsInactive = () => {
    setTimeWentInactive(new Date());
    performAutoLogout();
    clearInterval(inactivityTimer.current as number);
    inactivityTimer.current = false;
  };

  const handleMoveShouldSetPanResponder = useCallback(() => {
    setIsActive();
    return false;
  }, [setIsActive]);

  const handleStartShouldSetPanResponder = useCallback(() => {
    setIsActive();
    return false;
  }, [setIsActive]);

  const panResponder = useMemo(() =>
    PanResponder.create({
      onStartShouldSetPanResponder: handleStartShouldSetPanResponder,
      onMoveShouldSetPanResponder: handleMoveShouldSetPanResponder,
      onStartShouldSetPanResponderCapture: () => false,
      onMoveShouldSetPanResponderCapture: () => false,
      onPanResponderTerminationRequest: () => true,
      onShouldBlockNativeResponder: () => false,
    }), []);

  return {
    panResponder,
  };
}

Now, let's understand this code together.

useEffect(() => {
    if (token) {
      //  180 secs
      const autologoutTime = 180;
      waitForInactivity.current = autologoutTime * 1000;
    }
  }, [token, waitForInactivity.current]);

Here, we ensure that if token is present(i.e user is logged in), we set the autologoutTime which is in seconds. This is either gotten from the server or can be set manually. We then convert it to milliseconds and set it in our ref variable.

const checkInactive = useCallback(() => {
    if (inactivityTimer.current) {
      return;
    }
    inactivityTimer.current = setInterval(() => {
      if (Math.abs(new Date().valueOf() - lastInteraction.current.valueOf()) >=
        waitForInactivity.current) {
        setIsInactive();
      }
    }, INACTIVITY_CHECK_INTERVAL_MS);
  }, []);

The checkInactive method checks if the difference between the current time and the last time of interaction is greater than the window period for the app to be inactive. If this condition is fulfilled, then the app is said to be an inactive and and the setIsInactive method is called otherwise, the app is active. The setIsInactive method is where the logout is performed.

The responder methods handleMoveShouldSetPanResponder and handleStartShouldSetPanResponder are called when there is interaction(activity) in the app. In each of these methods, setIsActive method is called. This updates the lastInteraction value to the current date and also checks for the inactivity in the app. With this cycle, the app will remain active but if it is inactive for the 180 secs(or whatever time set), the app logs out by itself.

Finally, to make this applicable in our app, we have to import this useAutoLogout.ts hook in the root component of the app when the user is logged in and use it as below.

import React from 'react';
import { View } from 'react-native';
import { panResponder } from 'useAutoLogout.ts'

const Home = () => {
   return (
    <View {...panResponder.panHandlers}>
    /* screens. */
   </View>
   )
}

Conclusion

We have been able to add the auto-logout function to our application using PanResponder from React-Native. This code has been written using typescript but can be converted to javascript as well. For more references, you can visit the React-native documentation on PanResponders