Authentication Domain
Complete authentication implementation with hooks, services, and type-safe patterns
Authentication Domain
Production-ready authentication patterns for React Native applications
Overview
This domain demonstrates a complete authentication system using modern React Native patterns. Includes JWT token management, secure storage, type-safe hooks, and error handling.
Implementation Files
Types and Interfaces
// core/domains/auth/types.ts
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
role: 'user' | 'admin';
createdAt: string;
lastLoginAt: string;
}
export interface AuthTokens {
accessToken: string;
refreshToken: string;
expiresAt: number;
}
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterData {
email: string;
password: string;
name: string;
}
export interface AuthState {
user: User | null;
tokens: AuthTokens | null;
isLoading: boolean;
isAuthenticated: boolean;
}
export interface AuthError {
code: 'INVALID_CREDENTIALS' | 'TOKEN_EXPIRED' | 'NETWORK_ERROR' | 'UNKNOWN';
message: string;
details?: any;
}Service Layer
// core/domains/auth/services/authService.ts
import { ApiClient } from '@/core/shared/api/ApiClient';
import { SecureStorage } from '@/core/shared/storage/SecureStorage';
import type {
User,
AuthTokens,
LoginCredentials,
RegisterData,
AuthError
} from '../types';
export class AuthService {
private apiClient: ApiClient;
private storage: SecureStorage;
constructor() {
this.apiClient = new ApiClient();
this.storage = new SecureStorage();
}
async login(credentials: LoginCredentials): Promise<{
user: User;
tokens: AuthTokens;
}> {
try {
const response = await this.apiClient.post('/auth/login', credentials);
// Store tokens securely
await this.storage.setTokens(response.tokens);
return {
user: response.user,
tokens: response.tokens,
};
} catch (error) {
throw this.handleAuthError(error);
}
}
async register(data: RegisterData): Promise<{
user: User;
tokens: AuthTokens;
}> {
try {
const response = await this.apiClient.post('/auth/register', data);
await this.storage.setTokens(response.tokens);
return {
user: response.user,
tokens: response.tokens,
};
} catch (error) {
throw this.handleAuthError(error);
}
}
async logout(): Promise<void> {
try {
// Call logout endpoint to invalidate tokens
await this.apiClient.post('/auth/logout');
} catch (error) {
// Continue with local logout even if API call fails
console.warn('Logout API call failed:', error);
} finally {
// Always clear local storage
await this.storage.clearTokens();
}
}
async refreshTokens(): Promise<AuthTokens> {
try {
const currentTokens = await this.storage.getTokens();
if (!currentTokens?.refreshToken) {
throw new Error('No refresh token available');
}
const response = await this.apiClient.post('/auth/refresh', {
refreshToken: currentTokens.refreshToken,
});
await this.storage.setTokens(response.tokens);
return response.tokens;
} catch (error) {
// Clear invalid tokens
await this.storage.clearTokens();
throw this.handleAuthError(error);
}
}
async getCurrentUser(): Promise<User> {
try {
const response = await this.apiClient.get('/auth/me');
return response.user;
} catch (error) {
throw this.handleAuthError(error);
}
}
async updateProfile(updates: Partial<Pick<User, 'name' | 'avatar'>>): Promise<User> {
try {
const response = await this.apiClient.patch('/auth/profile', updates);
return response.user;
} catch (error) {
throw this.handleAuthError(error);
}
}
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
try {
await this.apiClient.post('/auth/change-password', {
currentPassword,
newPassword,
});
} catch (error) {
throw this.handleAuthError(error);
}
}
private handleAuthError(error: any): AuthError {
if (error.response?.status === 401) {
return {
code: 'INVALID_CREDENTIALS',
message: 'Invalid email or password',
details: error.response.data,
};
}
if (error.response?.status === 403) {
return {
code: 'TOKEN_EXPIRED',
message: 'Your session has expired',
details: error.response.data,
};
}
if (!error.response) {
return {
code: 'NETWORK_ERROR',
message: 'Network connection failed',
details: error,
};
}
return {
code: 'UNKNOWN',
message: error.message || 'An unexpected error occurred',
details: error,
};
}
}Custom Hooks
// core/domains/auth/hooks/useAuth.ts
import { useState, useEffect, useCallback } from 'react';
import { AuthService } from '../services/authService';
import { SecureStorage } from '@/core/shared/storage/SecureStorage';
import type {
User,
AuthTokens,
AuthState,
LoginCredentials,
RegisterData,
AuthError
} from '../types';
export function useAuth() {
const [state, setState] = useState<AuthState>({
user: null,
tokens: null,
isLoading: true,
isAuthenticated: false,
});
const authService = new AuthService();
const storage = new SecureStorage();
// Initialize auth state on mount
useEffect(() => {
initializeAuth();
}, []);
const initializeAuth = useCallback(async () => {
try {
setState(prev => ({ ...prev, isLoading: true }));
const tokens = await storage.getTokens();
if (!tokens) {
setState(prev => ({
...prev,
isLoading: false,
isAuthenticated: false
}));
return;
}
// Check if tokens are expired
if (tokens.expiresAt <= Date.now()) {
await authService.refreshTokens();
// Get updated tokens after refresh
const newTokens = await storage.getTokens();
if (!newTokens) {
setState(prev => ({
...prev,
isLoading: false,
isAuthenticated: false
}));
return;
}
tokens = newTokens;
}
// Get current user
const user = await authService.getCurrentUser();
setState({
user,
tokens,
isLoading: false,
isAuthenticated: true,
});
} catch (error) {
console.error('Auth initialization failed:', error);
setState({
user: null,
tokens: null,
isLoading: false,
isAuthenticated: false,
});
}
}, []);
const login = useCallback(async (credentials: LoginCredentials) => {
try {
setState(prev => ({ ...prev, isLoading: true }));
const { user, tokens } = await authService.login(credentials);
setState({
user,
tokens,
isLoading: false,
isAuthenticated: true,
});
return { user, tokens };
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
isAuthenticated: false
}));
throw error;
}
}, []);
const register = useCallback(async (data: RegisterData) => {
try {
setState(prev => ({ ...prev, isLoading: true }));
const { user, tokens } = await authService.register(data);
setState({
user,
tokens,
isLoading: false,
isAuthenticated: true,
});
return { user, tokens };
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
isAuthenticated: false
}));
throw error;
}
}, []);
const logout = useCallback(async () => {
try {
setState(prev => ({ ...prev, isLoading: true }));
await authService.logout();
setState({
user: null,
tokens: null,
isLoading: false,
isAuthenticated: false,
});
} catch (error) {
// Even if logout fails, clear local state
setState({
user: null,
tokens: null,
isLoading: false,
isAuthenticated: false,
});
throw error;
}
}, []);
const updateProfile = useCallback(async (updates: Partial<Pick<User, 'name' | 'avatar'>>) => {
try {
const updatedUser = await authService.updateProfile(updates);
setState(prev => ({
...prev,
user: updatedUser,
}));
return updatedUser;
} catch (error) {
throw error;
}
}, []);
return {
// State
user: state.user,
tokens: state.tokens,
isLoading: state.isLoading,
isAuthenticated: state.isAuthenticated,
// Actions
login,
register,
logout,
updateProfile,
refresh: initializeAuth,
};
}Authentication Context
// core/domains/auth/context/AuthContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { useAuth } from '../hooks/useAuth';
import type { User, LoginCredentials, RegisterData } from '../types';
interface AuthContextValue {
user: User | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (credentials: LoginCredentials) => Promise<any>;
register: (data: RegisterData) => Promise<any>;
logout: () => Promise<void>;
updateProfile: (updates: Partial<Pick<User, 'name' | 'avatar'>>) => Promise<User>;
refresh: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const auth = useAuth();
return (
<AuthContext.Provider value={auth}>
{children}
</AuthContext.Provider>
);
}
export function useAuthContext(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuthContext must be used within an AuthProvider');
}
return context;
}Usage Examples
Basic Authentication Flow
// features/auth/screens/LoginScreen.tsx
import React, { useState } from 'react';
import { View, Alert } from 'react-native';
import { Input } from '@/ui/foundation/Input';
import { Button } from '@/ui/foundation/Button';
import { useAuthContext } from '@/core/domains/auth/context/AuthContext';
export function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
const { login, isLoading } = useAuthContext();
const handleLogin = async () => {
try {
setErrors({});
await login({ email, password });
// Navigation handled by auth state change
} catch (error: any) {
if (error.code === 'INVALID_CREDENTIALS') {
setErrors({
email: 'Invalid email or password',
password: 'Invalid email or password'
});
} else {
Alert.alert('Login Failed', error.message);
}
}
};
return (
<View style={{ padding: 20 }}>
<Input
label="Email"
value={email}
onChangeText={setEmail}
error={errors.email}
keyboardType="email-address"
autoCapitalize="none"
/>
<Input
label="Password"
value={password}
onChangeText={setPassword}
error={errors.password}
secureTextEntry
style={{ marginTop: 16 }}
/>
<Button
onPress={handleLogin}
loading={isLoading}
style={{ marginTop: 24 }}
>
Sign In
</Button>
</View>
);
}Protected Route Pattern
// core/domains/auth/components/ProtectedRoute.tsx
import React, { ReactNode } from 'react';
import { View, Text } from 'react-native';
import { useAuthContext } from '../context/AuthContext';
import { LoadingSpinner } from '@/ui/foundation/LoadingSpinner';
interface ProtectedRouteProps {
children: ReactNode;
fallback?: ReactNode;
}
export function ProtectedRoute({
children,
fallback = <LoginPrompt />
}: ProtectedRouteProps) {
const { isAuthenticated, isLoading } = useAuthContext();
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<LoadingSpinner />
<Text style={{ marginTop: 16 }}>Authenticating...</Text>
</View>
);
}
if (!isAuthenticated) {
return <>{fallback}</>;
}
return <>{children}</>;
}
function LoginPrompt() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Please sign in to access this content</Text>
</View>
);
}Testing Patterns
// core/domains/auth/__tests__/useAuth.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import { useAuth } from '../hooks/useAuth';
import { AuthService } from '../services/authService';
// Mock the service
jest.mock('../services/authService');
const mockAuthService = AuthService as jest.MockedClass<typeof AuthService>;
describe('useAuth', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('initializes with loading state', () => {
const { result } = renderHook(() => useAuth());
expect(result.current.isLoading).toBe(true);
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBe(null);
});
it('handles successful login', async () => {
const mockUser = { id: '1', email: 'test@example.com', name: 'Test User' };
const mockTokens = { accessToken: 'token', refreshToken: 'refresh', expiresAt: Date.now() + 3600000 };
mockAuthService.prototype.login.mockResolvedValue({
user: mockUser,
tokens: mockTokens,
});
const { result } = renderHook(() => useAuth());
await act(async () => {
await result.current.login({
email: 'test@example.com',
password: 'password123',
});
});
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.user).toEqual(mockUser);
expect(result.current.isLoading).toBe(false);
});
it('handles login failure', async () => {
const mockError = {
code: 'INVALID_CREDENTIALS',
message: 'Invalid credentials',
};
mockAuthService.prototype.login.mockRejectedValue(mockError);
const { result } = renderHook(() => useAuth());
await act(async () => {
try {
await result.current.login({
email: 'test@example.com',
password: 'wrongpassword',
});
} catch (error) {
expect(error).toEqual(mockError);
}
});
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBe(null);
});
});Best Practices
Security Considerations
- ✅ Store tokens in secure storage (Keychain/Keystore)
- ✅ Implement automatic token refresh
- ✅ Clear sensitive data on logout
- ✅ Validate tokens on app resume
- ✅ Use HTTPS for all auth endpoints
Error Handling
- ✅ Distinguish between different error types
- ✅ Provide user-friendly error messages
- ✅ Handle network failures gracefully
- ✅ Implement retry logic for recoverable errors
Performance
- ✅ Lazy load auth service dependencies
- ✅ Memoize auth context values
- ✅ Avoid unnecessary re-renders
- ✅ Cache user data appropriately
Ready to use? Copy these patterns into your src/core/domains/auth/ directory and customize the API endpoints and business logic for your specific needs.