rn-auth
React Native Authentication (Expo)
Core Patterns
Expo AuthSession for OAuth
Use expo-auth-session with expo-web-browser for OAuth flows:
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
import * as Google from 'expo-auth-session/providers/google';
// Critical: Call this at module level for proper redirect handling
WebBrowser.maybeCompleteAuthSession();
// Inside component
const [request, response, promptAsync] = Google.useAuthRequest({
iosClientId: 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com',
webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com', // For backend verification
scopes: ['profile', 'email'],
});
Common Pitfalls
- Missing
maybeCompleteAuthSession()- Auth redirects fail silently without this at module level - Wrong client ID - iOS needs the iOS client ID, but backend verification needs the web client ID
- Scheme mismatch -
app.jsonscheme must match Google Cloud Console redirect URI - Expo Go vs standalone - Different redirect URIs; use
AuthSession.makeRedirectUri()to handle both
Token Storage
Use expo-secure-store for tokens (not AsyncStorage):
import * as SecureStore from 'expo-secure-store';
const TOKEN_KEY = 'auth_token';
const REFRESH_KEY = 'refresh_token';
export const tokenStorage = {
async save(token: string, refresh?: string) {
await SecureStore.setItemAsync(TOKEN_KEY, token);
if (refresh) {
await SecureStore.setItemAsync(REFRESH_KEY, refresh);
}
},
async get() {
return SecureStore.getItemAsync(TOKEN_KEY);
},
async getRefresh() {
return SecureStore.getItemAsync(REFRESH_KEY);
},
async clear() {
await SecureStore.deleteItemAsync(TOKEN_KEY);
await SecureStore.deleteItemAsync(REFRESH_KEY);
},
};
Auth Context Pattern
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
type AuthState = {
token: string | null;
user: User | null;
isLoading: boolean;
signIn: (token: string, user: User) => Promise<void>;
signOut: () => Promise<void>;
};
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [token, setToken] = useState<string | null>(null);
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Restore session on mount
async function restore() {
try {
const savedToken = await tokenStorage.get();
if (savedToken) {
// Validate token with backend before trusting it
const userData = await validateToken(savedToken);
setToken(savedToken);
setUser(userData);
}
} catch {
await tokenStorage.clear();
} finally {
setIsLoading(false);
}
}
restore();
}, []);
const signIn = async (newToken: string, userData: User) => {
await tokenStorage.save(newToken);
setToken(newToken);
setUser(userData);
};
const signOut = async () => {
await tokenStorage.clear();
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ token, user, isLoading, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be inside AuthProvider');
return ctx;
};
Protected Routes with Expo Router
// app/_layout.tsx
import { Slot, useRouter, useSegments } from 'expo-router';
import { useAuth } from '@/contexts/auth';
import { useEffect } from 'react';
export default function RootLayout() {
const { token, isLoading } = useAuth();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (isLoading) return;
const inAuthGroup = segments[0] === '(auth)';
if (!token && !inAuthGroup) {
router.replace('/(auth)/login');
} else if (token && inAuthGroup) {
router.replace('/(app)/home');
}
}, [token, isLoading, segments]);
if (isLoading) {
return <LoadingScreen />;
}
return <Slot />;
}
Backend Integration
Sending Auth Headers
// api/client.ts
import { tokenStorage } from '@/utils/tokenStorage';
const API_BASE = process.env.EXPO_PUBLIC_API_URL;
async function authFetch(path: string, options: RequestInit = {}) {
const token = await tokenStorage.get();
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
});
if (response.status === 401) {
// Token expired - try refresh or force logout
const refreshed = await attemptTokenRefresh();
if (!refreshed) {
await tokenStorage.clear();
// Trigger auth state update (emit event or use callback)
}
}
return response;
}
Google Token Verification (FastAPI backend)
# For reference: backend should verify Google tokens like this
from google.oauth2 import id_token
from google.auth.transport import requests
def verify_google_token(token: str, client_id: str) -> dict:
"""Verify Google ID token and return user info."""
idinfo = id_token.verify_oauth2_token(
token,
requests.Request(),
client_id # Use WEB client ID here, not iOS
)
return {
"google_id": idinfo["sub"],
"email": idinfo["email"],
"name": idinfo.get("name"),
}
Debugging Auth Issues
Check redirect URI configuration
// Log the redirect URI being used
console.log('Redirect URI:', AuthSession.makeRedirectUri());
Compare this with what's configured in:
- Google Cloud Console > Credentials > OAuth 2.0 Client IDs
app.jsonscheme field
Common error patterns
| Error | Likely Cause |
|---|---|
| "redirect_uri_mismatch" | Redirect URI in console doesn't match app |
| Auth popup opens but nothing happens | Missing maybeCompleteAuthSession() |
| Works in Expo Go, fails in build | Using Expo Go redirect URI in standalone config |
| Token validation fails on backend | Using iOS client ID instead of web client ID for verification |
Test auth flow
- Clear all tokens:
await tokenStorage.clear() - Force kill app
- Reopen and verify redirect to login
- Complete sign-in flow
- Force kill and reopen - should stay logged in
More from aiskillstore/marketplace
skill-creator
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.
502xlsx
Comprehensive spreadsheet creation, editing, and analysis with support for formulas, formatting, data analysis, and visualization. When Claude needs to work with spreadsheets (.xlsx, .xlsm, .csv, .tsv, etc) for: (1) Creating new spreadsheets with formulas and formatting, (2) Reading or analyzing data, (3) Modify existing spreadsheets while preserving formulas, (4) Data analysis and visualization in spreadsheets, or (5) Recalculating formulas
220frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
218pptx
Presentation creation, editing, and analysis. When Claude needs to work with presentations (.pptx files) for: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes, or any other presentation tasks
209docx
Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks
202skill-development
This skill should be used when the user wants to "create a skill", "add a skill to plugin", "write a new skill", "improve skill description", "organize skill content", or needs guidance on skill structure, progressive disclosure, or skill development best practices for Claude Code plugins.
183