Curso de Supabase Avanzado

Toma las primeras clases gratis
<h1>Autenticación con OAuth y SSR en Supabase</h1>

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:

  1. Crea credenciales OAuth 2.0
  2. 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

Toma las primeras clases gratis

0 Comentarios

para escribir tu comentario

Artículos relacionados