Awesome-omni-skill secure-storage-patterns
expo-secure-store patterns for sensitive data. Use when storing tokens and credentials.
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/secure-storage-patterns" ~/.claude/skills/diegosouzapw-awesome-omni-skill-secure-storage-patterns && rm -rf "$T"
manifest:
skills/development/secure-storage-patterns/SKILL.mdsource content
Secure Storage Patterns Skill
This skill covers secure data storage for React Native with Expo.
When to Use
Use this skill when:
- Storing authentication tokens
- Saving sensitive user data
- Managing API keys
- Storing encryption keys
Core Principle
SECURE BY DEFAULT - Always use SecureStore for sensitive data, never AsyncStorage.
Installation
npx expo install expo-secure-store
Basic Usage
import * as SecureStore from 'expo-secure-store'; // Save value await SecureStore.setItemAsync('authToken', 'your-token-here'); // Get value const token = await SecureStore.getItemAsync('authToken'); // Delete value await SecureStore.deleteItemAsync('authToken');
Authentication Token Storage
// lib/secureStorage.ts import * as SecureStore from 'expo-secure-store'; const TOKEN_KEY = 'authToken'; const REFRESH_TOKEN_KEY = 'refreshToken'; const USER_KEY = 'user'; export const secureStorage = { // Token management async saveToken(token: string): Promise<void> { await SecureStore.setItemAsync(TOKEN_KEY, token); }, async getToken(): Promise<string | null> { return SecureStore.getItemAsync(TOKEN_KEY); }, async deleteToken(): Promise<void> { await SecureStore.deleteItemAsync(TOKEN_KEY); }, // Refresh token async saveRefreshToken(token: string): Promise<void> { await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, token); }, async getRefreshToken(): Promise<string | null> { return SecureStore.getItemAsync(REFRESH_TOKEN_KEY); }, async deleteRefreshToken(): Promise<void> { await SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY); }, // User data async saveUser(user: User): Promise<void> { await SecureStore.setItemAsync(USER_KEY, JSON.stringify(user)); }, async getUser(): Promise<User | null> { const userStr = await SecureStore.getItemAsync(USER_KEY); return userStr ? JSON.parse(userStr) : null; }, async deleteUser(): Promise<void> { await SecureStore.deleteItemAsync(USER_KEY); }, // Clear all auth data async clearAuth(): Promise<void> { await Promise.all([ SecureStore.deleteItemAsync(TOKEN_KEY), SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY), SecureStore.deleteItemAsync(USER_KEY), ]); }, };
Auth Store with Secure Storage
import { create } from 'zustand'; import { secureStorage } from '@/lib/secureStorage'; interface User { id: string; email: string; name: string; } interface AuthState { user: User | null; token: string | null; isAuthenticated: boolean; isLoading: boolean; initialize: () => Promise<void>; login: (email: string, password: string) => Promise<void>; logout: () => Promise<void>; } export const useAuthStore = create<AuthState>((set) => ({ user: null, token: null, isAuthenticated: false, isLoading: true, initialize: async () => { try { const [token, user] = await Promise.all([ secureStorage.getToken(), secureStorage.getUser(), ]); if (token && user) { set({ token, user, isAuthenticated: true, isLoading: false, }); } else { set({ isLoading: false }); } } catch { set({ isLoading: false }); } }, login: async (email, password) => { set({ isLoading: true }); const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (!response.ok) { set({ isLoading: false }); throw new Error('Login failed'); } const { user, token, refreshToken } = await response.json(); await Promise.all([ secureStorage.saveToken(token), secureStorage.saveRefreshToken(refreshToken), secureStorage.saveUser(user), ]); set({ user, token, isAuthenticated: true, isLoading: false, }); }, logout: async () => { await secureStorage.clearAuth(); set({ user: null, token: null, isAuthenticated: false, }); }, }));
Initialize Auth on App Start
// app/_layout.tsx import { useEffect } from 'react'; import { useAuthStore } from '@/store/authStore'; export default function RootLayout(): React.ReactElement { const initialize = useAuthStore((state) => state.initialize); const isLoading = useAuthStore((state) => state.isLoading); useEffect(() => { initialize(); }, [initialize]); if (isLoading) { return <LoadingScreen />; } return <Stack />; }
Token Refresh Pattern
// lib/api.ts import axios from 'axios'; import { secureStorage } from './secureStorage'; const api = axios.create({ baseURL: process.env.EXPO_PUBLIC_API_URL, }); // Request interceptor - add token api.interceptors.request.use(async (config) => { const token = await secureStorage.getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Response interceptor - refresh token on 401 api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshToken = await secureStorage.getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token'); } const response = await axios.post('/api/auth/refresh', { refreshToken, }); const { token: newToken } = response.data; await secureStorage.saveToken(newToken); originalRequest.headers.Authorization = `Bearer ${newToken}`; return api(originalRequest); } catch { await secureStorage.clearAuth(); // Navigate to login return Promise.reject(error); } } return Promise.reject(error); } ); export default api;
Biometric Authentication
import * as LocalAuthentication from 'expo-local-authentication'; export const biometricAuth = { async isAvailable(): Promise<boolean> { const compatible = await LocalAuthentication.hasHardwareAsync(); const enrolled = await LocalAuthentication.isEnrolledAsync(); return compatible && enrolled; }, async authenticate(): Promise<boolean> { const result = await LocalAuthentication.authenticateAsync({ promptMessage: 'Authenticate to continue', fallbackLabel: 'Use passcode', }); return result.success; }, }; // Usage with secure storage async function getSecureData(): Promise<string | null> { const authenticated = await biometricAuth.authenticate(); if (!authenticated) { throw new Error('Authentication failed'); } return secureStorage.getToken(); }
Storage Options
// SecureStore options await SecureStore.setItemAsync('key', 'value', { keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK, }); // Available options: // AFTER_FIRST_UNLOCK - accessible after device unlocked once // AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY - same, but not synced // ALWAYS - always accessible (less secure) // ALWAYS_THIS_DEVICE_ONLY - always accessible, not synced // WHEN_UNLOCKED - only when device is unlocked (default) // WHEN_UNLOCKED_THIS_DEVICE_ONLY - same, but not synced
When to Use What
// ✅ SecureStore - Sensitive data await SecureStore.setItemAsync('authToken', token); await SecureStore.setItemAsync('apiKey', key); await SecureStore.setItemAsync('encryptionKey', key); // ❌ AsyncStorage - Non-sensitive data only await AsyncStorage.setItem('theme', 'dark'); await AsyncStorage.setItem('onboardingComplete', 'true'); await AsyncStorage.setItem('lastViewedProduct', productId);
Notes
- Always use SecureStore for tokens and credentials
- SecureStore has ~2KB limit per item
- Data is encrypted using device keychain/keystore
- Consider biometric authentication for extra security
- Clear sensitive data on logout
- Handle storage errors gracefully