Capítulo 7: Configuración de Clerk - Autenticación Moderna
Capítulo 7: Configuración de Clerk - Autenticación Moderna
En este capítulo configuraremos Clerk, una plataforma de autenticación moderna que nos proporcionará un sistema completo de autenticación con OAuth, verificación de email, gestión de usuarios y mucho más.
🎯 Objetivos del Capítulo
- Configurar Clerk en nuestro proyecto React Native
- Establecer providers de OAuth (Google, Apple)
- Implementar el ClerkProvider y configuración inicial
- Configurar variables de entorno para desarrollo y producción
- Establecer testing para componentes con autenticación
📋 Prerrequisitos
- Capítulos 1-6 completados
- Conocimiento básico de autenticación
- Cuenta de Google Cloud Platform (para OAuth)
- Cuenta de Apple Developer (para Apple Sign-In)
🚀 Configuración de Cuenta Clerk
Paso 1: Crear Cuenta en Clerk
- Ve a clerk.com y crea una cuenta gratuita
- Crea una nueva aplicación llamada “TodoApp”
- Selecciona “React Native” como plataforma
- Anota las claves que aparecen (las necesitaremos más adelante)
Paso 2: Configuración Inicial en Clerk Dashboard
// Configuración que haremos en el dashboard:
// 1. Enable Email/Password authentication
// 2. Enable Google OAuth
// 3. Enable Apple OAuth (para iOS)
// 4. Configure redirect URLs
// 5. Set up email templates
📦 Instalación de Dependencias
Instalemos las dependencias necesarias para Clerk:
# Instalar Clerk para Expo
npx expo install @clerk/clerk-expo
# Instalar dependencias para OAuth
npx expo install expo-linking expo-auth-session expo-crypto
# Instalar Web Browser para OAuth flows
npx expo install expo-web-browser
# Instalar Secure Store para tokens
npx expo install expo-secure-store
Actualicemos nuestro package.json con las nuevas dependencias:
{
"dependencies": {
"@clerk/clerk-expo": "^0.19.14",
"expo-linking": "~6.2.2",
"expo-auth-session": "~5.4.0",
"expo-crypto": "~12.8.1",
"expo-web-browser": "~12.8.2",
"expo-secure-store": "~12.9.0"
}
}
🔧 Configuración de Variables de Entorno
Creemos nuestro archivo de configuración de entorno:
// .env.local
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
# Para desarrollo
CLERK_SECRET_KEY=sk_test_your_secret_key_here
# URLs de redirect para OAuth
EXPO_PUBLIC_REDIRECT_URL=exp://localhost:19000/--/oauth-callback
Actualicemos nuestro app.json para incluir el esquema de URL:
{
"expo": {
"name": "TodoApp",
"slug": "todo-app",
"scheme": "todoapp",
"platforms": ["ios", "android"],
"extra": {
"clerkPublishableKey": process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY
}
}
}
🏗️ Configuración del ClerkProvider
Creemos el proveedor principal de Clerk:
// src/providers/ClerkProvider.tsx
import React from 'react';
import { ClerkProvider as BaseClerkProvider } from '@clerk/clerk-expo';
import Constants from 'expo-constants';
import * as SecureStore from 'expo-secure-store';
// Configuración de token cache
const tokenCache = {
async getToken(key: string) {
try {
return SecureStore.getItemAsync(key);
} catch (err) {
return null;
}
},
async saveToken(key: string, value: string) {
try {
return SecureStore.setItemAsync(key, value);
} catch (err) {
return;
}
},
};
interface ClerkProviderProps {
children: React.ReactNode;
}
export const ClerkProvider: React.FC<ClerkProviderProps> = ({ children }) => {
const publishableKey = Constants.expoConfig?.extra?.clerkPublishableKey;
if (!publishableKey) {
throw new Error(
'Missing Publishable Key. Please set EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env'
);
}
return (
<BaseClerkProvider
publishableKey={publishableKey}
tokenCache={tokenCache}
>
{children}
</BaseClerkProvider>
);
};
🔐 Configuración de OAuth Providers
Google OAuth Setup
// src/config/oauth.ts
export const OAuthConfig = {
google: {
iosClientId: 'your-ios-client-id.googleusercontent.com',
androidClientId: 'your-android-client-id.googleusercontent.com',
webClientId: 'your-web-client-id.googleusercontent.com',
},
apple: {
clientId: 'com.yourcompany.todoapp',
redirectUri: 'https://your-app.clerk.accounts.dev/oauth_callback',
},
} as const;
Configuración en Google Cloud Console
// Pasos para configurar Google OAuth:
// 1. Ir a Google Cloud Console
// 2. Crear nuevo proyecto o seleccionar existente
// 3. Habilitar Google+ API
// 4. Crear credenciales OAuth 2.0
// 5. Configurar pantalla de consentimiento
// 6. Agregar redirect URIs de Clerk
🏪 Integración con Zustand
Actualicemos nuestro store para incluir el estado de autenticación:
// src/stores/authStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { User } from '@clerk/clerk-expo';
interface AuthState {
// Estado de autenticación
isAuthenticated: boolean;
user: User | null;
isLoading: boolean;
// Acciones
setUser: (user: User | null) => void;
setLoading: (loading: boolean) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
devtools(
persist(
immer((set) => ({
// Estado inicial
isAuthenticated: false,
user: null,
isLoading: true,
// Acciones
setUser: (user) =>
set((state) => {
state.user = user;
state.isAuthenticated = !!user;
state.isLoading = false;
}),
setLoading: (loading) =>
set((state) => {
state.isLoading = loading;
}),
logout: () =>
set((state) => {
state.user = null;
state.isAuthenticated = false;
state.isLoading = false;
}),
})),
{
name: 'auth-storage',
storage: {
getItem: async (name) => {
const value = await AsyncStorage.getItem(name);
return value ? JSON.parse(value) : null;
},
setItem: async (name, value) => {
await AsyncStorage.setItem(name, JSON.stringify(value));
},
removeItem: async (name) => {
await AsyncStorage.removeItem(name);
},
},
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
}
),
{ name: 'auth-store' }
)
);
🎣 Hook Personalizado para Autenticación
Creemos un hook que combine Clerk con nuestro store:
// src/hooks/useAuth.ts
import { useEffect } from 'react';
import { useUser, useAuth as useClerkAuth } from '@clerk/clerk-expo';
import { useAuthStore } from '../stores/authStore';
export const useAuth = () => {
const { user, isLoaded } = useUser();
const { signOut } = useClerkAuth();
const { setUser, setLoading, logout, ...authState } = useAuthStore();
useEffect(() => {
if (isLoaded) {
setUser(user);
} else {
setLoading(true);
}
}, [user, isLoaded, setUser, setLoading]);
const handleLogout = async () => {
try {
await signOut();
logout();
} catch (error) {
console.error('Error during logout:', error);
}
};
return {
...authState,
user,
isLoaded,
logout: handleLogout,
};
};
🧪 Configuración de Testing con Clerk
Configuremos mocks para testing con Clerk:
// __mocks__/@clerk/clerk-expo.ts
export const useUser = jest.fn(() => ({
user: null,
isLoaded: true,
}));
export const useAuth = jest.fn(() => ({
isLoaded: true,
isSignedIn: false,
signOut: jest.fn(),
}));
export const ClerkProvider = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export const SignedIn = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export const SignedOut = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
Utilidades de Testing
// src/utils/test-utils.tsx
import React from 'react';
import { render, RenderOptions } from '@testing-library/react-native';
import { ClerkProvider } from '../providers/ClerkProvider';
// Mock del Constants para testing
jest.mock('expo-constants', () => ({
expoConfig: {
extra: {
clerkPublishableKey: 'pk_test_mock_key',
},
},
}));
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
return <ClerkProvider>{children}</ClerkProvider>;
};
const customRender = (
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllTheProviders, ...options });
export * from '@testing-library/react-native';
export { customRender as render };
🧪 Tests para la Configuración
// src/hooks/__tests__/useAuth.test.ts
import { renderHook } from '@testing-library/react-native';
import { useAuth } from '../useAuth';
import { useUser, useAuth as useClerkAuth } from '@clerk/clerk-expo';
jest.mock('@clerk/clerk-expo');
const mockedUseUser = useUser as jest.MockedFunction<typeof useUser>;
const mockedUseClerkAuth = useClerkAuth as jest.MockedFunction<typeof useClerkAuth>;
describe('useAuth', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return initial state when not loaded', () => {
mockedUseUser.mockReturnValue({
user: null,
isLoaded: false,
});
mockedUseClerkAuth.mockReturnValue({
signOut: jest.fn(),
});
const { result } = renderHook(() => useAuth());
expect(result.current.isLoading).toBe(true);
expect(result.current.isAuthenticated).toBe(false);
expect(result.current.user).toBe(null);
});
it('should update state when user is loaded', () => {
const mockUser = {
id: 'user_123',
emailAddresses: [{ emailAddress: '[email protected]' }],
firstName: 'John',
lastName: 'Doe',
};
mockedUseUser.mockReturnValue({
user: mockUser,
isLoaded: true,
});
mockedUseClerkAuth.mockReturnValue({
signOut: jest.fn(),
});
const { result } = renderHook(() => useAuth());
expect(result.current.isLoading).toBe(false);
expect(result.current.isAuthenticated).toBe(true);
expect(result.current.user).toBe(mockUser);
});
it('should handle logout correctly', async () => {
const mockSignOut = jest.fn();
mockedUseClerkAuth.mockReturnValue({
signOut: mockSignOut,
});
mockedUseUser.mockReturnValue({
user: null,
isLoaded: true,
});
const { result } = renderHook(() => useAuth());
await result.current.logout();
expect(mockSignOut).toHaveBeenCalled();
});
});
🎨 Componente de Estado de Autenticación
Creemos un componente para mostrar el estado de autenticación:
// src/components/AuthStatus.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useAuth } from '../hooks/useAuth';
export const AuthStatus: React.FC = () => {
const { isAuthenticated, user, isLoading } = useAuth();
if (isLoading) {
return (
<View style={styles.container}>
<Text style={styles.text}>Cargando autenticación...</Text>
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.text}>
Estado: {isAuthenticated ? 'Autenticado' : 'No autenticado'}
</Text>
{user && (
<Text style={styles.userText}>
Usuario: {user.firstName} {user.lastName}
</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 16,
backgroundColor: '#f5f5f5',
borderRadius: 8,
margin: 16,
},
text: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
},
userText: {
fontSize: 14,
color: '#666',
},
});
🔧 Actualización del App Principal
Actualicemos nuestro componente principal para incluir Clerk:
// App.tsx
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { ClerkProvider } from './src/providers/ClerkProvider';
import { MainNavigator } from './src/navigation/MainNavigator';
import { AuthStatus } from './src/components/AuthStatus';
export default function App() {
return (
<SafeAreaProvider>
<ClerkProvider>
<AuthStatus />
<MainNavigator />
</ClerkProvider>
</SafeAreaProvider>
);
}
📱 Testing en Dispositivo
Para probar la configuración:
# Ejecutar en desarrollo
npx expo start
# Verificar que no hay errores de configuración
# El componente AuthStatus debería mostrar "No autenticado"
# Para testing en dispositivo real
npx expo start --tunnel
🔍 Verificación de la Configuración
Checklist para verificar que todo está configurado correctamente:
- Clerk Provider configurado sin errores
- Variables de entorno cargadas correctamente
- Store de autenticación funcionando
- Hook useAuth retornando estado correcto
- Tests pasando sin errores
- AuthStatus mostrando estado correcto
📝 Configuración de Desarrollo vs Producción
// src/config/environment.ts
import Constants from 'expo-constants';
export const Environment = {
isDevelopment: __DEV__,
clerkPublishableKey: Constants.expoConfig?.extra?.clerkPublishableKey,
// URLs base según entorno
apiUrl: __DEV__
? 'http://localhost:3000/api'
: 'https://your-api.com/api',
// Configuración de OAuth según entorno
oauthRedirectUrl: __DEV__
? 'exp://localhost:19000/--/oauth-callback'
: 'todoapp://oauth-callback',
} as const;
🚀 Siguientes Pasos
En el próximo capítulo implementaremos:
- Componentes de SignIn y SignUp
- Flujos de autenticación completos
- OAuth con Google y Apple
- Verificación de email y teléfono
- Protección de rutas
📚 Recursos Adicionales
🎯 Resumen del Capítulo
Hemos establecido una base sólida para la autenticación en nuestra aplicación:
✅ Configuración completa de Clerk ✅ Variables de entorno y configuración ✅ Integración con Zustand para estado global ✅ Hook personalizado para autenticación ✅ Testing configurado con mocks ✅ Componentes de estado de autenticación
La aplicación ahora tiene una infraestructura de autenticación robusta y lista para implementar los flujos de login y registro en el siguiente capítulo.
Próximo capítulo: Implementación de Autenticación - Crearemos los componentes de SignIn, SignUp y flujos OAuth completos.