UTA DevHub

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.

On this page