React Native Authentication Flow

Authentication proves who a user is. A sign-in screen, a protected home screen, and a logout button form the core of the experience. The app must remember a signed-in user across restarts, hide private screens from visitors, and keep the login credentials safe. A clear flow handles all three jobs.

The Big Picture

App starts
    │
    ▼
Check for saved token
    │
 ┌──┴────────────┐
 │               │
Token found    No token
 │               │
Home screens   Login screens
 │               │
 │      Sign in succeeds → save token → Home screens
 │
Logout → delete token → Login screens

A token is a long random string issued by the server after a successful login. Think of it as a wristband at a festival. Show the wristband at each gate, and staff let you through without asking for your ticket again.

Two Navigation Groups

Split screens into a public group and a private group. Show only one group at a time.

function RootNavigator() {
  const { token, loading } = useAuth();

  if (loading) return <SplashScreen />;

  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      {token ? (
        <Stack.Screen name="Home" component={HomeScreen} />
      ) : (
        <>
          <Stack.Screen name="Login" component={LoginScreen} />
          <Stack.Screen name="Register" component={RegisterScreen} />
        </>
      )}
    </Stack.Navigator>
  );
}

The screen list changes the moment the token appears or disappears. React Navigation moves the user automatically, so no manual redirect is needed.

Build an Auth Context

Share the sign-in state across the app with a context provider. It exposes the token plus signIn and signOut functions.

import * as SecureStore from 'expo-secure-store';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [token, setToken] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const restore = async () => {
      const saved = await SecureStore.getItemAsync('token');
      setToken(saved);
      setLoading(false);
    };
    restore();
  }, []);

  const signIn = async (email, password) => {
    const res = await fetch('https://example.com/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    const data = await res.json();
    await SecureStore.setItemAsync('token', data.token);
    setToken(data.token);
  };

  const signOut = async () => {
    await SecureStore.deleteItemAsync('token');
    setToken(null);
  };

  return (
    <AuthContext.Provider value={{ token, loading, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

Store Tokens Securely

AsyncStorage keeps data as plain text, which suits settings and cached lists. Tokens deserve stronger protection. expo-secure-store saves values in the iOS Keychain and the Android Keystore, both encrypted by the operating system.

Data Type              Best Storage
──────────────────────────────────────────
Theme, language        AsyncStorage
Cached article list    AsyncStorage
Login token            SecureStore
Refresh token          SecureStore

The Login Screen

function LoginScreen() {
  const { signIn } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleLogin = async () => {
    try {
      await signIn(email, password);
    } catch (e) {
      setError('Email or password is incorrect');
    }
  };

  return (
    <View style={{ padding: 20 }}>
      <TextInput placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" />
      <TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
      {error ? <Text style={{ color: 'red' }}>{error}</Text> : null}
      <Button title="Sign In" onPress={handleLogin} />
    </View>
  );
}

Send the Token with Requests

Attach the token to the header of every private request. The server reads it and identifies the user.

fetch('https://example.com/profile', {
  headers: { Authorization: `Bearer ${token}` },
});

Handle Expired Tokens

Tokens expire after a set time. A server reply with status 401 signals an expired or invalid token. Catch that status, clear the saved token, and return the user to the login screen. Many apps also use a refresh token to request a new token silently.

Security Checklist

  • Send credentials over HTTPS only
  • Store tokens in SecureStore, never in plain storage
  • Never print tokens or passwords to logs
  • Keep secret keys on the server, never inside app code
  • Show generic error text that does not reveal which field was wrong
  • Consider trusted providers such as Firebase Auth or OAuth sign-in for added safety

Summary

An authentication flow checks for a saved token at startup, shows public screens to visitors, and shows private screens to signed-in users. Build an auth context, keep tokens in secure storage, send them in request headers, and handle expiry by returning users to the login screen.