Mobile Auth & Navigation
Mobile authentication is fundamentally different from the web. There are no cookies and no localStorage. In this course, you will learn how to store tokens securely, build login and logout flows, protect routes, and create tab and stack navigation with Expo Router.
Auth in Mobile Apps
On the web, you store JWT tokens in cookies or localStorage. On mobile, neither exists. Instead, mobile apps use encrypted device storage that is protected by the operating system itself.
This means your authentication tokens are more secure on mobile than on web. The Keychain/Keystore is hardware-backed on modern devices, making it extremely difficult to extract tokens.
Challenge: Understand the Difference
Explain in your own words why mobile apps cannot use cookies or localStorage. What does expo-secure-store use instead on iOS vs Android?
Login Flow
The login flow on mobile follows four steps:
- 1. User enters email and password on the login screen
- 2. App calls
POST /api/auth/loginwith the credentials - 3. Store the returned tokens in SecureStore
- 4. Navigate to the home screen
import * as SecureStore from 'expo-secure-store'
async function handleLogin(email: string, password: string) {
const response = await fetch(API_URL + "/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
})
const data = await response.json()
// Store tokens in encrypted storage
await SecureStore.setItemAsync('access_token', data.access_token)
await SecureStore.setItemAsync('refresh_token', data.refresh_token)
// Navigate to home
router.replace('/(tabs)')
}router.replace() is used instead of router.push() so the user cannot press the back button to return to the login screen after logging in.Challenge: Find Token Storage
Find where tokens are stored in the scaffolded code. What key names are used for the access token and refresh token? What file contains this logic?
Challenge: Test the Login
Register a user (if you haven't already), then log in. Check the API server logs — do you see the login request? Does the app navigate away from the login screen?
Auth Context
The auth state needs to be accessible from any screen in the app. Grit uses the React Context pattern — an AuthProvider wraps the entire app and provides auth state to all screens through a useAuth hook.
// hooks/useAuth.tsx
interface AuthContextType {
user: User | null
isLoading: boolean
isAuthenticated: boolean
login: (email: string, password: string) => Promise<void>
register: (email: string, password: string, name: string) => Promise<void>
logout: () => Promise<void>
}
// In any screen:
const { user, isAuthenticated, logout } = useAuth()The AuthProvider reads tokens from SecureStore on app launch. If valid tokens exist, the user is automatically logged in. If not, they see the login screen.
Challenge: Find the Auth Context
Find the auth context or useAuth hook in the scaffolded code. What values does it provide? Where is the AuthProvider mounted?
Protected Routes
Screens inside (tabs)/ should only be accessible to logged-in users. The tab layout checks the auth state and redirects to login if the user is not authenticated:
// app/(tabs)/_layout.tsx
import { Redirect } from 'expo-router'
import { useAuth } from '@/hooks/useAuth'
export default function TabLayout() {
const { user, isLoading } = useAuth()
// Show loading screen while checking auth
if (isLoading) return <LoadingScreen />
// Redirect to login if not authenticated
if (!user) return <Redirect href="/login" />
return (
<Tabs>
<Tabs.Screen name="index" options={{ title: "Home" }} />
<Tabs.Screen name="profile" options={{ title: "Profile" }} />
<Tabs.Screen name="settings" options={{ title: "Settings" }} />
</Tabs>
)
}Challenge: Test Protected Routes
Log out of the app and try to access a protected screen (like the home tab). Does it redirect you to the login screen? Log in again — do you return to the protected screen?
Expo Router Navigation
Mobile apps use two primary navigation patterns:
Here's how the file structure maps to navigation:
app/
├── _layout.tsx ← Root layout (Stack)
├── login.tsx ← Login screen
├── register.tsx ← Register screen
└── (tabs)/
├── _layout.tsx ← Tab bar layout
├── index.tsx ← Home tab
├── profile.tsx ← Profile tab
└── settings.tsx ← Settings tabThe root layout uses a Stack navigator. Login and register are stack screens. The (tabs)/ group contains a Tab navigator with three tabs. This means you can have stack navigation (push/pop screens) at the root level, and tab navigation inside the authenticated area.
Challenge: Add a New Tab
Create a new file at app/(tabs)/explore.tsx with a simple screen component. Then add it to the (tabs)/_layout.tsx tab configuration. Does it appear in the tab bar?
Challenge: Stack Inside a Tab
Inside one of your tabs, add a button that navigates to a detail screen using router.push(). Does the back button appear? Can you navigate back?
Token Refresh
The access token expires in 15 minutes. When it expires, the API returns a401 Unauthorized response. Instead of logging the user out, the API client automatically uses the refresh token to get a new access token.
// lib/api.ts — simplified interceptor pattern
async function fetchWithAuth(url: string, options: RequestInit = {}) {
const token = await SecureStore.getItemAsync('access_token')
const response = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: "Bearer " + token },
})
// If token expired, refresh and retry
if (response.status === 401) {
const newToken = await refreshAccessToken()
if (newToken) {
return fetch(url, {
...options,
headers: { ...options.headers, Authorization: "Bearer " + newToken },
})
}
}
return response
}
async function refreshAccessToken(): Promise<string | null> {
const refreshToken = await SecureStore.getItemAsync('refresh_token')
const response = await fetch(API_URL + "/api/auth/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
})
const data = await response.json()
if (data.access_token) {
await SecureStore.setItemAsync('access_token', data.access_token)
return data.access_token
}
return null
}Challenge: Find the Refresh Logic
Look at the API client in the scaffolded code. Can you find the refresh token logic? What happens if the refresh token itself has expired?
Logout
Logging out involves three steps: clear tokens from SecureStore, reset the auth state, and navigate to the login screen.
async function logout() {
// 1. Clear tokens from encrypted storage
await SecureStore.deleteItemAsync('access_token')
await SecureStore.deleteItemAsync('refresh_token')
// 2. Reset auth state
setUser(null)
// 3. Navigate to login
router.replace('/login')
}router.replace() when navigating to login after logout. This prevents the user from pressing the back button to return to a protected screen.Challenge: Test Logout
Log in, then log out. After logging out, try pressing the back button. Can you access the protected screens? You should not be able to.
Summary
Here's what you learned in this course:
- Mobile apps use SecureStore (encrypted device storage) instead of cookies
- The auth context provides user state to all screens via
useAuth - Protected routes redirect unauthenticated users to login
- Tab navigation is the primary pattern; stack navigation is for drill-downs
- Token refresh happens automatically — users stay logged in seamlessly
Challenge: Build a Complete Auth Flow
Verify your app has: login screen, register screen, protected tab navigation (Home, Profile, Settings), and a logout button in the settings or profile screen.
Challenge: Add a New Protected Screen
Add a "Workouts" tab to the tab bar. It should show a simple list screen. Verify that it's protected — logging out and accessing it directly should redirect to login.
Challenge: Test Token Expiry
If possible, set the access token expiry to a short duration (e.g., 1 minute) in the API config. Use the app until the token expires. Does the refresh happen transparently? Does the app stay logged in?
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.