Building a Secure "Remember Me"
When building authentication flows, one of the most common user requests is "Can you just remember my login?" It seems simple enough at first — save the credentials somewhere and reload them next time. But like most things in software development and in life, the devil is in the details.
The Storage Dilemma: AsyncStorage vs Expo SecureStore
Before diving into implementation, I had to make a decision: where to store user credentials. The two main contenders were:
- AsyncStorage - React Native's simple key-value storage
- Expo SecureStore - secure storage
AsyncStorage might seem like the obvious choice—it's simpler, widely used, and gets the job done. But here's the thing: AsyncStorage stores data in plain text. On a rooted anyone with file system access can read these credentials.
Expo SecureStore, on the other hand, leverages the device's secure hardware enclave (Keychain on iOS, Keystore on Android). Even if someone gains file system access, the credentials remain encrypted and protected by the operating system's security layer.
Creating the Secure Storage Utility
First, I created small helper function around Expo SecureStore:
import * as SecureStore from 'expo-secure-store';
// Save credentials securely
const save = async (key: string, value: string) => {
await SecureStore.setItemAsync(key, value);
};
// Retrieve saved credentials
const getValueFor = async (key: string) => {
let result = await SecureStore.getItemAsync(key);
if (result) return result;
};
// Delete credentials when user unchecks "remember me"
const deleteValue = async (key: string) => {
await SecureStore.deleteItemAsync(key);
};
export {save, getValueFor, deleteValue};
This utility provides three essential operations: save, retrieve, and delete. The delete function is particularly important—when users change their mind about being remembered.
Integrating the Checkbox UI
I added a checkbox component that's both functional and accessible:
import {Button, TextInput, Checkbox} from 'react-native-paper';
import {save, getValueFor, deleteValue} from '@/utils/secureStorage';
// State for remembering user preference
const [rememberUser, setRememberUser] = useState(false);
// Checkbox with accessible label
<View style={styles.checkboxContainer}>
<Checkbox
status={rememberUser ? 'checked' : 'unchecked'}
onPress={handleCheckbox}
/>
<Text style={styles.checkboxLabel} onPress={handleCheckbox}>
Zapamiętaj mnie
</Text>
</View>
Checkbox Handler
The checkbox handler is where it's not just about toggling a boolean—it's about managing sensitive data also:
const handleCheckbox = async () => {
const newRememberValue = !rememberUser;
setRememberUser(newRememberValue);
// Immediately delete stored credentials when unchecked
if (!newRememberValue) {
try {
await deleteValue('rememberedEmail');
await deleteValue('rememberedPassword');
} catch (error) {
console.log('Error clearing saved credentials');
}
}
};
The key insight here is immediate deletion. When users uncheck "remember me," their credentials are deleted instantly—not on the next login attempt, not when the app restarts, but immediately.
Loading Saved Credentials on Mount
The component needs to check for saved credentials when it first loads:
useEffect(() => {
const loadSavedCredentials = async () => {
try {
const savedEmail = await getValueFor('rememberedEmail');
const savedPassword = await getValueFor('rememberedPassword');
if (savedEmail && savedPassword) {
setData({
email: savedEmail,
password: savedPassword,
});
setRememberUser(true);
}
} catch (error) {
console.log('No saved credentials found');
}
};
loadSavedCredentials();
}, []);
Saving Credentials on Successful Login
Finally, the login handler saves credentials only after successful authentication:
const handleSave = async () => {
try {
await dispatch(signIn(data)).unwrap();
// Only save if user opted in AND login succeeded
if (rememberUser) {
await save('rememberedEmail', data.email);
await save('rememberedPassword', data.password);
}
router.replace('/');
} catch (error: any) {
console.error('error', error?.message || 'Coś poszło nie tak');
}
};