mobile-auth-patterns
SKILL.md
Mobile Auth Patterns
Comprehensive skill for implementing authentication in React Native/Expo mobile apps.
Overview
Mobile authentication requires special considerations:
- Secure token storage (not AsyncStorage)
- Biometric authentication for quick access
- Social login providers (Apple, Google)
- Session management across app states
- Refresh token handling
Use When
This skill is automatically invoked when:
- Setting up authentication flows
- Implementing biometric unlock
- Integrating social login providers
- Managing secure token storage
- Handling session persistence
Auth Provider Templates
Clerk Integration
// providers/ClerkProvider.tsx
import { ClerkProvider, useAuth } from '@clerk/clerk-expo';
import * as SecureStore from 'expo-secure-store';
const tokenCache = {
async getToken(key: string) {
return await SecureStore.getItemAsync(key);
},
async saveToken(key: string, value: string) {
await SecureStore.setItemAsync(key, value);
},
async clearToken(key: string) {
await SecureStore.deleteItemAsync(key);
},
};
export function AuthProvider({ children }: { children: React.ReactNode }) {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!;
return (
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
{children}
</ClerkProvider>
);
}
// hooks/useAuthenticatedUser.ts
import { useUser, useAuth } from '@clerk/clerk-expo';
export function useAuthenticatedUser() {
const { user, isLoaded } = useUser();
const { isSignedIn, signOut, getToken } = useAuth();
return {
user,
isLoaded,
isSignedIn,
signOut,
getToken,
fullName: user?.fullName,
email: user?.primaryEmailAddress?.emailAddress,
avatar: user?.imageUrl,
};
}
Supabase Auth
// lib/supabase.ts
import 'react-native-url-polyfill/auto';
import { createClient } from '@supabase/supabase-js';
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
const ExpoSecureStoreAdapter = {
getItem: async (key: string) => {
return await SecureStore.getItemAsync(key);
},
setItem: async (key: string, value: string) => {
await SecureStore.setItemAsync(key, value);
},
removeItem: async (key: string) => {
await SecureStore.deleteItemAsync(key);
},
};
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: ExpoSecureStoreAdapter,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});
// hooks/useSupabaseAuth.ts
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import { Session, User } from '@supabase/supabase-js';
export function useSupabaseAuth() {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user ?? null);
setIsLoading(false);
});
// Listen for auth changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setUser(session?.user ?? null);
});
return () => subscription.unsubscribe();
}, []);
const signIn = async (email: string, password: string) => {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
};
const signUp = async (email: string, password: string) => {
const { error } = await supabase.auth.signUp({ email, password });
if (error) throw error;
};
const signOut = async () => {
const { error } = await supabase.auth.signOut();
if (error) throw error;
};
return {
session,
user,
isLoading,
isAuthenticated: !!session,
signIn,
signUp,
signOut,
};
}
Biometric Authentication
// lib/biometrics.ts
import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
export interface BiometricCapabilities {
isAvailable: boolean;
biometryType: 'fingerprint' | 'face' | 'iris' | null;
isEnrolled: boolean;
}
export async function getBiometricCapabilities(): Promise<BiometricCapabilities> {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
const supportedTypes =
await LocalAuthentication.supportedAuthenticationTypesAsync();
let biometryType: BiometricCapabilities['biometryType'] = null;
if (
supportedTypes.includes(
LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
)
) {
biometryType = 'face';
} else if (
supportedTypes.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)
) {
biometryType = 'fingerprint';
} else if (
supportedTypes.includes(LocalAuthentication.AuthenticationType.IRIS)
) {
biometryType = 'iris';
}
return {
isAvailable: hasHardware && isEnrolled,
biometryType,
isEnrolled,
};
}
export async function authenticateWithBiometrics(
promptMessage = 'Authenticate to continue'
): Promise<boolean> {
try {
const result = await LocalAuthentication.authenticateAsync({
promptMessage,
cancelLabel: 'Cancel',
disableDeviceFallback: false,
fallbackLabel: 'Use passcode',
});
return result.success;
} catch (error) {
console.error('Biometric authentication error:', error);
return false;
}
}
// Biometric-protected secure storage
export const BiometricSecureStore = {
async setItem(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value, {
requireAuthentication: true,
authenticationPrompt: 'Authenticate to save credentials',
});
},
async getItem(key: string): Promise<string | null> {
try {
return await SecureStore.getItemAsync(key, {
requireAuthentication: true,
authenticationPrompt: 'Authenticate to access credentials',
});
} catch {
return null;
}
},
};
Social Login (Apple & Google)
// lib/socialAuth.ts
import * as AppleAuthentication from 'expo-apple-authentication';
import * as Google from 'expo-auth-session/providers/google';
import { supabase } from './supabase';
// Apple Sign In
export async function signInWithApple() {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
if (credential.identityToken) {
const { data, error } = await supabase.auth.signInWithIdToken({
provider: 'apple',
token: credential.identityToken,
});
if (error) throw error;
return data;
}
} catch (e: any) {
if (e.code === 'ERR_REQUEST_CANCELED') {
// User cancelled
return null;
}
throw e;
}
}
// Google Sign In (with Clerk)
import { useOAuth } from '@clerk/clerk-expo';
import * as WebBrowser from 'expo-web-browser';
import * as Linking from 'expo-linking';
WebBrowser.maybeCompleteAuthSession();
export function useGoogleAuth() {
const { startOAuthFlow } = useOAuth({ strategy: 'oauth_google' });
const signInWithGoogle = async () => {
try {
const { createdSessionId, setActive } = await startOAuthFlow({
redirectUrl: Linking.createURL('/oauth-callback'),
});
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId });
return true;
}
return false;
} catch (error) {
console.error('Google sign in error:', error);
throw error;
}
};
return { signInWithGoogle };
}
Complete Auth Context
// contexts/AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import { getBiometricCapabilities, authenticateWithBiometrics } from '@/lib/biometrics';
import * as SecureStore from 'expo-secure-store';
interface AuthContextType {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
biometricsEnabled: boolean;
biometricType: string | null;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
signInWithBiometrics: () => Promise<boolean>;
enableBiometrics: () => Promise<void>;
disableBiometrics: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [biometricsEnabled, setBiometricsEnabled] = useState(false);
const [biometricType, setBiometricType] = useState<string | null>(null);
useEffect(() => {
initializeAuth();
}, []);
async function initializeAuth() {
// Check session
const { data: { session } } = await supabase.auth.getSession();
setUser(session?.user ?? null);
// Check biometric status
const capabilities = await getBiometricCapabilities();
setBiometricType(capabilities.biometryType);
const enabled = await SecureStore.getItemAsync('biometrics_enabled');
setBiometricsEnabled(enabled === 'true');
setIsLoading(false);
// Listen for changes
supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null);
});
}
async function signIn(email: string, password: string) {
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) throw error;
}
async function signUp(email: string, password: string) {
const { error } = await supabase.auth.signUp({ email, password });
if (error) throw error;
}
async function signOut() {
await supabase.auth.signOut();
await SecureStore.deleteItemAsync('biometric_session');
setBiometricsEnabled(false);
}
async function signInWithBiometrics() {
if (!biometricsEnabled) return false;
const authenticated = await authenticateWithBiometrics();
if (authenticated) {
const sessionToken = await SecureStore.getItemAsync('biometric_session');
if (sessionToken) {
const { error } = await supabase.auth.setSession(JSON.parse(sessionToken));
return !error;
}
}
return false;
}
async function enableBiometrics() {
const { data } = await supabase.auth.getSession();
if (data.session) {
await SecureStore.setItemAsync('biometric_session', JSON.stringify(data.session));
await SecureStore.setItemAsync('biometrics_enabled', 'true');
setBiometricsEnabled(true);
}
}
async function disableBiometrics() {
await SecureStore.deleteItemAsync('biometric_session');
await SecureStore.setItemAsync('biometrics_enabled', 'false');
setBiometricsEnabled(false);
}
return (
<AuthContext.Provider
value={{
user,
isLoading,
isAuthenticated: !!user,
biometricsEnabled,
biometricType,
signIn,
signUp,
signOut,
signInWithBiometrics,
enableBiometrics,
disableBiometrics,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Best Practices
-
Token Storage
- Always use SecureStore, never AsyncStorage for tokens
- Implement secure token refresh logic
- Clear tokens on sign out
-
Biometrics
- Check capabilities before showing option
- Provide fallback authentication
- Store session encrypted, not credentials
-
Social Login
- Use native sign-in where available (Apple)
- Handle deep link callbacks properly
- Request minimal scopes
-
Security
- Implement certificate pinning for production
- Use HTTPS for all API calls
- Validate tokens server-side
Weekly Installs
4
Repository
vanman2024/ai-d…ketplaceGitHub Stars
3
First Seen
Feb 11, 2026
Security Audits
Installed on
opencode4
gemini-cli4
claude-code4
github-copilot4
codex4
amp4