Caso de Uso Real
Imagina que estás construyendo una aplicación SaaS de gestión de proyectos donde los usuarios necesitan iniciar sesión con Google o GitHub. Necesitas que la autenticación funcione correctamente en Server-Side Rendering (SSR) para:
- Proteger rutas del lado del servidor
- Mantener sesiones persistentes entre recargas
- Evitar flashes de contenido no autenticado
- Mejorar SEO con contenido protegido renderizado en servidor
Arquitectura de la Solución
¿Por qué SSR con OAuth?
- OAuth requiere redirecciones que deben manejarse en el servidor
- SSR permite verificar autenticación antes de renderizar la página
- Evita el patrón anti-seguridad de verificar auth solo en cliente
- Cookies HTTP-only protegen tokens mejor que localStorage
1. Configuración Inicial de Supabase
Configuración de OAuth Provider (Google)
Primero, configura OAuth en tu proyecto Supabase:
En Google Cloud Console:
- Crea credenciales OAuth 2.0
- Agrega URIs autorizadas:
https://tu-proyecto.supabase.co/auth/v1/callback
En Supabase Dashboard:
Authentication > Providers > Google
- Activa el provider
- Pega Client ID y Client Secret
Variables de Entorno
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://tu-proyecto.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=tu-anon-key
2. Configuración del Cliente Supabase para SSR
¿Por qué dos tipos de clientes?
- Cliente: Para componentes del navegador
- Server: Para Server Components y API routes
- Cada uno maneja cookies de forma diferente según el contexto
Cliente para Browser
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
Cliente para Server
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createServerSupabaseClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
// Ocurre en Server Components, se maneja en middleware
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {
// Ocurre en Server Components, se maneja en middleware
}
},
},
}
)
}
Best Practice: El manejo de cookies en try-catch es necesario porque en Server Components de Next.js no puedes modificar cookies directamente. El middleware se encarga de esto.
3. Middleware para Refrescar Sesiones
¿Por qué middleware?
- Intercepta TODAS las requests antes de llegar a las páginas
- Refresca tokens automáticamente si están por expirar
- Actualiza cookies en cada request (crucial para SSR)
// middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({
name,
value,
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value,
...options,
})
},
remove(name: string, options: CookieOptions) {
request.cookies.set({
name,
value: '',
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value: '',
...options,
})
},
},
}
)
// Refresca la sesión si existe
await supabase.auth.getUser()
return response
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
4. Implementación del Flujo OAuth
Botón de Login (Client Component)
// components/auth/google-login-button.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
export function GoogleLoginButton() {
const supabase = createClient()
const handleGoogleLogin = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
})
if (error) {
console.error('Error al iniciar sesión:', error.message)
}
}
return (
<button
onClick={handleGoogleLogin}
className="px-4 py-2 bg-white border rounded-lg hover:bg-gray-50"
>
Continuar con Google
</button>
)
}
Best Practice: access_type: 'offline' y prompt: 'consent' aseguran que obtengas un refresh token de Google para mantener sesiones largas.
Route Handler para Callback
// app/auth/callback/route.ts
import { createServerSupabaseClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
const origin = requestUrl.origin
if (code) {
const supabase = await createServerSupabaseClient()
// Intercambia el código por una sesión
await supabase.auth.exchangeCodeForSession(code)
}
// Redirige al dashboard o donde necesites
return NextResponse.redirect(`${origin}/dashboard`)
}
¿Por qué exchangeCodeForSession?
- OAuth devuelve un código temporal, no tokens directamente
- Este método intercambia el código por access_token y refresh_token
- Almacena automáticamente los tokens en cookies seguras
5. Protección de Rutas (Server Component)
// app/dashboard/page.tsx
import { createServerSupabaseClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const supabase = await createServerSupabaseClient()
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) {
redirect('/login')
}
return (
<div>
<h1>Dashboard de {user.email}</h1>
<p>Provider: {user.app_metadata.provider}</p>
</div>
)
}
Best Practice: Verifica autenticación en Server Components para:
- Evitar flashes de contenido
- Proteger datos sensibles desde el servidor
- Mejorar UX con redirecciones instantáneas
6. Logout
// components/auth/logout-button.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
export function LogoutButton() {
const supabase = createClient()
const router = useRouter()
const handleLogout = async () => {
await supabase.auth.signOut()
router.refresh()
router.push('/login')
}
return (
<button onClick={handleLogout}>
Cerrar Sesión
</button>
)
}
router.refresh() fuerza a Next.js a re-ejecutar Server Components con el nuevo estado de autenticación.
7. Configuración de Políticas RLS (Row Level Security)
¿Por qué RLS?
- Protege datos a nivel de base de datos, no solo en código
- Cada query verifica automáticamente permisos
- Previene acceso no autorizado incluso si hay bugs en tu app
-- Habilitar RLS en tabla de proyectos
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Política: Los usuarios solo ven sus propios proyectos
CREATE POLICY "Users can view own projects"
ON projects
FOR SELECT
USING (auth.uid() = user_id);
-- Política: Los usuarios solo pueden crear proyectos para sí mismos
CREATE POLICY "Users can insert own projects"
ON projects
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Política: Los usuarios solo pueden actualizar sus proyectos
CREATE POLICY "Users can update own projects"
ON projects
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
Consulta con RLS desde el Cliente
// RLS se aplica automáticamente
const { data: projects } = await supabase
.from('projects')
.select('*')
// Solo retorna proyectos donde user_id = auth.uid()
8. Manejo de Perfiles de Usuario
-- Tabla de perfiles que se crea automáticamente al registrarse
CREATE TABLE profiles (
id UUID REFERENCES auth.users ON DELETE CASCADE PRIMARY KEY,
email TEXT,
full_name TEXT,
avatar_url TEXT,
provider TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- RLS para perfiles
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles FOR SELECT
USING (true);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Trigger para crear perfil automáticamente
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email, full_name, avatar_url, provider)
VALUES (
NEW.id,
NEW.email,
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'avatar_url',
NEW.raw_app_meta_data->>'provider'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Ejecutar trigger después de signup
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
Best Practice: SECURITY DEFINER permite que el trigger escriba en la tabla aunque el usuario no tenga permisos directos.
Checklist de Implementación
- [ ] Configurar OAuth provider en Supabase Dashboard
- [ ] Crear clientes de Supabase (browser y server)
- [ ] Implementar middleware para refrescar sesiones
- [ ] Crear route handler
/auth/callback - [ ] Proteger rutas en Server Components
- [ ] Configurar RLS en todas las tablas
- [ ] Crear trigger para perfiles automáticos
- [ ] Implementar logout funcional
- [ ] Probar flujo completo en incógnito
Errores Comunes y Soluciones
Error: “Auth session missing”
Causa: Middleware no configurado correctamente
Solución: Verifica que el matcher del middleware incluya tus rutas
Error: “PKCE flow failed”
Causa: Callback URL incorrecta
Solución: La URL debe ser exactamente https://tu-proyecto.supabase.co/auth/v1/callback
Error: Políticas RLS bloquean inserts
Causa: Política WITH CHECK muy restrictiva
Solución: Asegúrate que auth.uid() esté disponible en el contexto
Recursos Adicionales
Conclusión: Este patrón de OAuth + SSR es production-ready y escala bien. La clave está en separar correctamente cliente/servidor y dejar que RLS maneje la seguridad a nivel de datos.
Curso de Supabase Avanzado
COMPARTE ESTE ARTÍCULO Y MUESTRA LO QUE APRENDISTE




