Auth system (JWT + refresh + secure storage)
Hereβs a production-grade auth system for Expo + Bun using:
- π JWT (access + refresh tokens)
- π Auto refresh flow
- π Secure storage (device-safe)
- π§ State via Zustand
- β‘ API handling via TanStack Query
This is how real fintech / SaaS apps handle auth.
π§ 1. Architecture Overview
Section titled βπ§ 1. Architecture OverviewβLogin β Receive access + refresh token βStore securely (SecureStore) βAttach access token to API calls βIf expired β use refresh token βIf refresh fails β logout userπ 2. Secure Storage Layer
Section titled βπ 2. Secure Storage Layerβπ src/services/storage/secure.ts
Section titled βπ src/services/storage/secure.tsβimport * as SecureStore from 'expo-secure-store';
export const secureStorage = { set: async (key: string, value: string) => { await SecureStore.setItemAsync(key, value); },
get: async (key: string) => { return await SecureStore.getItemAsync(key); },
remove: async (key: string) => { await SecureStore.deleteItemAsync(key); },};π§ 3. Auth Store (Zustand)
Section titled βπ§ 3. Auth Store (Zustand)βπ src/features/auth/store.ts
Section titled βπ src/features/auth/store.tsβimport { create } from 'zustand';
type AuthState = { accessToken: string | null; refreshToken: string | null; setTokens: (access: string, refresh: string) => void; logout: () => void;};
export const useAuthStore = create<AuthState>((set) => ({ accessToken: null, refreshToken: null,
setTokens: (access, refresh) => set({ accessToken: access, refreshToken: refresh }),
logout: () => set({ accessToken: null, refreshToken: null }),}));π 4. API Client with Auto Token Injection
Section titled βπ 4. API Client with Auto Token Injectionβπ src/services/api/client.ts
Section titled βπ src/services/api/client.tsβimport axios from 'axios';import { useAuthStore } from '@/features/auth/store';
export const apiClient = axios.create({ baseURL: 'https://api.example.com',});
apiClient.interceptors.request.use((config) => { const token = useAuthStore.getState().accessToken;
if (token) { config.headers.Authorization = `Bearer ${token}`; }
return config;});π 5. Refresh Token Logic (CRITICAL)
Section titled βπ 5. Refresh Token Logic (CRITICAL)βπ src/services/api/refresh.ts
Section titled βπ src/services/api/refresh.tsβimport { apiClient } from './client';import { useAuthStore } from '@/features/auth/store';
let isRefreshing = false;let queue: any[] = [];
export const refreshToken = async () => { const { refreshToken, setTokens, logout } = useAuthStore.getState();
if (!refreshToken) return logout();
try { const { data } = await apiClient.post('/auth/refresh', { refreshToken, });
setTokens(data.accessToken, data.refreshToken); return data.accessToken; } catch (e) { logout(); throw e; }};
export const handleTokenRefresh = async (error: any) => { const originalRequest = error.config;
if (error.response?.status !== 401) { return Promise.reject(error); }
if (isRefreshing) { return new Promise((resolve) => { queue.push((token: string) => { originalRequest.headers.Authorization = `Bearer ${token}`; resolve(apiClient(originalRequest)); }); }); }
isRefreshing = true;
try { const newToken = await refreshToken();
queue.forEach((cb) => cb(newToken)); queue = [];
originalRequest.headers.Authorization = `Bearer ${newToken}`; return apiClient(originalRequest); } finally { isRefreshing = false; }};π Attach to Axios
Section titled βπ Attach to AxiosβapiClient.interceptors.response.use( (res) => res, async (error) => handleTokenRefresh(error));π 6. Persist Tokens Securely
Section titled βπ 6. Persist Tokens Securelyβπ src/features/auth/persist.ts
Section titled βπ src/features/auth/persist.tsβimport { secureStorage } from '@/services/storage/secure';import { useAuthStore } from './store';
const ACCESS = 'access_token';const REFRESH = 'refresh_token';
export const saveTokens = async (access: string, refresh: string) => { await secureStorage.set(ACCESS, access); await secureStorage.set(REFRESH, refresh);};
export const loadTokens = async () => { const access = await secureStorage.get(ACCESS); const refresh = await secureStorage.get(REFRESH);
if (access && refresh) { useAuthStore.getState().setTokens(access, refresh); }};
export const clearTokens = async () => { await secureStorage.remove(ACCESS); await secureStorage.remove(REFRESH);};π 7. Login Flow
Section titled βπ 7. Login Flowβπ src/features/auth/api.ts
Section titled βπ src/features/auth/api.tsβimport { apiClient } from '@/services/api/client';
export const login = async (email: string, password: string) => { const { data } = await apiClient.post('/auth/login', { email, password, });
return data;};π src/features/auth/hooks.ts
Section titled βπ src/features/auth/hooks.tsβimport { useMutation } from '@tanstack/react-query';import { login } from './api';import { saveTokens } from './persist';import { useAuthStore } from './store';
export const useLogin = () => { const setTokens = useAuthStore((s) => s.setTokens);
return useMutation({ mutationFn: ({ email, password }: any) => login(email, password),
onSuccess: async (data) => { setTokens(data.accessToken, data.refreshToken); await saveTokens(data.accessToken, data.refreshToken); }, });};πͺ 8. Logout Flow
Section titled βπͺ 8. Logout Flowβimport { clearTokens } from './persist';import { useAuthStore } from './store';
export const useLogout = () => { const logout = useAuthStore((s) => s.logout);
return async () => { logout(); await clearTokens(); };};π 9. App Initialization (Auto Login)
Section titled βπ 9. App Initialization (Auto Login)βπ app/_layout.tsx
Section titled βπ app/_layout.tsxβimport { useEffect, useState } from 'react';import { loadTokens } from '@/features/auth/persist';import { Stack } from 'expo-router';
export default function RootLayout() { const [ready, setReady] = useState(false);
useEffect(() => { loadTokens().finally(() => setReady(true)); }, []);
if (!ready) return null;
return <Stack />;}π‘οΈ 10. Protect Routes
Section titled βπ‘οΈ 10. Protect Routesβconst token = useAuthStore((s) => s.accessToken);
if (!token) { return <Redirect href="/login" />;}π Real Production Behavior
Section titled βπ Real Production Behaviorβ- App starts β tokens loaded from secure storage
- Requests auto include JWT
- Token expires β auto refresh
- Refresh fails β user logged out
- Offline β requests queued (if you implemented earlier system)
β οΈ Critical Best Practices
Section titled ββ οΈ Critical Best Practicesββ Always use refresh token rotation β Keep access token short-lived (5β15 min) β Never store tokens in plain AsyncStorage β Handle multi-request refresh (queue system) β Log auth failures (important for security)
π₯ Senior-Level Enhancements
Section titled βπ₯ Senior-Level Enhancementsβ- Biometric unlock (FaceID / Fingerprint)
- Token binding to device
- Silent background refresh
- Role-based access control (RBAC)
- Session timeout handling
If you want next level (true enterprise/mobile banking level), I can show you:
- π₯ OAuth (Google / Apple login via Expo)
- π§ Multi-device session management
- β‘ Web + Mobile shared auth architecture
- π End-to-end encrypted storage strategy