Configuración en profundidad de Supabase Auth: OAuth, SSO y control de permisos

Esa tarde, un colega de ventas se acercó a mí y me dijo: “El cliente requiere que Okta inicie sesión y el SSO estará en línea dentro de dos semanas”. Me quedé atónito por un momento: mi aplicación solo admite el registro por correo electrónico y Google OAuth, ¿SAML? Ningún contacto en absoluto.
Este escenario es muy común en B2B SaaS. Los clientes empresariales no aceptarán que sus empleados registren cuentas separadas. Disponen de un sistema unificado de gestión de identidades (Okta, Azure AD, Google Workspace). El inicio de sesión requiere un salto con un solo clic y las cuentas de los empleados caducan automáticamente cuando se van.
Este artículo está preparado para este escenario. Comenzaremos con el inicio de sesión social OAuth, pasaremos a la integración empresarial SAML SSO y, finalmente, usaremos Row Level Security (RLS) para lograr el aislamiento de permisos de múltiples inquilinos: una solución completa de autenticación y autorización que cubre todo, desde el nivel del consumidor hasta el nivel empresarial.
1. Práctica de configuración multiproveedor de OAuth
El inicio de sesión social OAuth es el punto de partida para la mayoría de las aplicaciones. Los usuarios no quieren recordar sus contraseñas y usted no quiere lidiar con el almacenamiento y la verificación de contraseñas; deje que Google o GitHub lo hagan, salvando a ambas partes.
Supabase soporta muchos proveedores de OAuth: Google, GitHub, Apple, Facebook, Discord, Twitter… Pero en entornos de producción reales, Google y GitHub son los más utilizados, mientras que Apple es un requisito obligatorio para las aplicaciones de iOS (requerido para la revisión de la App Store). Comencemos con Google.
Configuración de Google OAuth
Abra Google Cloud Console y el denso menú a veces resulta realmente confuso. No entre en pánico, simplemente busque “ID de cliente OAuth” para encontrar la entrada.
Al crear, seleccione el tipo Aplicación web. El paso clave es completar el URI de redireccionamiento autorizado:
https://<tu proyectoref>.supabase.co/auth/v1/callback
Esta dirección es donde el servicio Supabase Auth recibe devoluciones de llamada de OAuth. La referencia del proyecto se puede ver en la esquina superior izquierda del Panel de Supabase y se parece a “abcdefghijklmnop”.
Después de obtener la ID del cliente y el secreto del cliente, regrese al Panel de control de Supabase, busque Autenticación > Proveedores, habilite Google y complete estos dos valores.
La llamada del código es simple:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://abcdefghijklmnop.supabase.co',
'your-anon-key'
)
//Iniciar inicio de sesión OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://your-app.com/auth/callback',
scopes: 'email profile'
}
})
if (error) {
console.error('Error de inicio de sesión:', error.message)
return
}
// data.url es la dirección de salto, solo usa window.location.href = data.url
redirectTo es la dirección donde su aplicación recibe el resultado del inicio de sesión. Supabase pasará el código y los parámetros de estado allí. Debe llamar a supabase.auth.exchangeCodeForSession() en esa página para completar el inicio de sesión:
// En la página /auth/callback
const { error } = await supabase.auth.exchangeCodeForSession()
if (!error) {
// Inicia sesión exitosamente, salta a la página de inicio
window.location.href = '/'
}
Configuración de GitHub OAuth
La configuración de GitHub es similar y la entrada está en Configuración > Configuración de desarrollador > Aplicaciones OAuth. La URL de devolución de llamada de autorización completada al crear también es https://<project ref>.supabase.co/auth/v1/callback.
Hay una diferencia: la aplicación GitHub OAuth no tiene una interfaz de configuración de alcances, los alcances se especifican en el código:
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://your-app.com/auth/callback',
alcances: 'usuario del repositorio' // el repositorio puede acceder a almacenes privados
}
})
Si simplemente está iniciando sesión como usuario, usar los ámbitos predeterminados es suficiente. Si necesita acceder a los datos de GitHub del usuario (como sincronizar la lista de repositorios), debe agregar repo a los ámbitos.
Configuración de Apple OAuth
La configuración de Apple es la más problemática. Para crear una ID de servicios en el Portal de desarrolladores de Apple, también debe generar una clave privada (archivo .p8). La clave privada sólo se puede descargar una vez y deberás regenerarla si la pierdes.
Parámetros clave:
- ID de servicios: similar al ID de cliente
- ID del equipo: encuéntrelo en la página de Membresía
- Key ID: ID de la clave privada
- Clave privada: contenido del archivo .p8 descargado
Al completar estos valores en el Panel de Supabase, la clave privada debe copiar todo el contenido del archivo (incluidas las dos líneas COMIENZO/FIN).
El código de llamada es el mismo que el de Google/GitHub:
await supabase.auth.signInWithOAuth({
provider: 'apple',
options: {
redirectTo: 'https://your-app.com/auth/callback'
}
})
Hay otra forma para las aplicaciones de iOS: llame directamente a la API nativa de Iniciar sesión con Apple, obtenga el token de identidad y páselo a Supabase:
// El front-end recibe el token de identidad devuelto por Apple
const { data, error } = await supabase.auth.signInWithIdTokenCredentials({
provider: 'apple',
token: identityToken
})
Este método es adecuado para aplicaciones de iOS que tienen un inicio de sesión nativo de Apple integrado y pueden reutilizar la lógica existente.
2. Integración empresarial SAML SSO
Volviendo al principio: el cliente solicita iniciar sesión con Okta. En este punto, OAuth no es suficiente y se requiere SAML 2.0 SSO.
SAML funciona de manera completamente diferente a OAuth. OAuth es cuando los usuarios autorizan a aplicaciones de terceros a acceder a sus datos, mientras que SAML es cuando un proveedor de identidad empresarial (IdP) envía información de identidad del usuario a su aplicación (ServiceProvider, SP). Para las empresas, SAML es más controlable: cuando un usuario abandona la empresa, el IdP prohíbe la cuenta y todas las aplicaciones conectadas dejan de ser válidas automáticamente.
Preparativos antes de la configuración
Debe obtener los metadatos del IdP del cliente. Este archivo contiene el certificado del IdP, la dirección del punto final y otra información. Okta, Azure AD y Google Workspace tienen entradas para exportar metadatos.
URL clave proporcionada por Supabase:
ID de entidad (identificación del proveedor de servicios):
https://<projectref>.supabase.co/auth/v1/sso/saml/metadata
URL ACS (la dirección para recibir la respuesta SAML):
https://<projectref>.supabase.co/auth/v1/sso/saml/acs
URL de metadatos (descargable):
https://<referencia del proyecto>.supabase.co/auth/v1/sso/saml/metadata?download=true
Informe estas URL al administrador de TI del cliente y pídale que cree una aplicación SAML en Okta/Azure AD.
Configuración de SSO mediante Supabase CLI
Supabase Dashboard ahora también admite la configuración SAML, pero estoy más acostumbrado a usar CLI; después de todo, es posible que la configuración de los clientes empresariales deba depurarse repetidamente y la línea de comando es más controlable.
#Agregar conexión SAML
supabase sso agregar --type saml --project-ref <project ref> \
--metadata-url 'https://company.okta.com/app/exk123/saml/samlmetadata' \
--domains company.com
El parámetro --domains es importante. Le dice a Supabase que todos los usuarios del nombre de dominio company.com deben usar esta conexión SAML al iniciar sesión.
También puedes usar --metadata-file en lugar de --metadata-url para cargar archivos XML directamente:
supabase sso agregar --type saml --project-ref <project ref> \
--metadata-file ./okta-metadata.xml \
--domains company.com
Después de ejecutar el comando, se devolverá un sso_provider_id, similar a abc123def456. Este ID es la clave para el aislamiento de permisos RLS.
Configuración de asignación de atributos
La respuesta SAML contiene información del usuario (correo electrónico, nombre, departamento, etc.), pero es posible que la denominación de los campos no sea uniforme. Debe indicarle a Supabase cómo asignar estos campos.
Cree un archivo JSON:
{
"keys": {
"email": {
"name": "email",
"names": ["EmailAddress", "email", "mail"],
"required": true
},
"first_name": {
"name": "first_name",
"names": ["FirstName", "givenName", "first_name"]
},
"last_name": {
"name": "last_name",
"names": ["LastName", "surname", "last_name"]
}
}
}
Luego use supabase sso update para aplicar esta configuración:
supabase sso update <sso_provider_id> \
--project-ref <projectref> \
--attribute-mapping-file ./mapping.json
Configuración de SSO multiinquilino
Se pueden agregar varias conexiones SAML a un proyecto. Por ejemplo, tiene dos clientes empresariales, uno es Acme Corp (que usa Okta) y el otro es Globex Inc (que usa Azure AD):
# Agregar SSO de Acme Corp
supabase sso agregar --type saml --project-ref <project ref> \
--metadata-url 'https://acme.okta.com/.../metadata' \
--domains acme.com
# Devuelve sso_provider_id: proveedor_abc
# Agregar SSO para Globex Inc
supabase sso agregar --type saml --project-ref <project ref> \
--metadata-url 'https://globex.azure.com/.../metadata' \
--domains globex.com
# Devolver sso_provider_id: proveedor_def
Ahora los empleados de Acme Corp iniciarán sesión como “provider_abc” y los empleados de Globex Inc iniciarán sesión como “provider_def”. Los datos de usuario para cada conexión SSO están aislados.
Experiencia de inicio de sesión del usuario
Una vez completada la configuración, el proceso de inicio de sesión del usuario es el siguiente:
- El usuario ingresa la dirección de correo electrónico (como
[email protected]) - Supabase detecta que el nombre de dominio
acme.comtiene configuración SSO - Saltar automáticamente a la página de inicio de sesión de Okta de Acme
- El usuario ingresa la contraseña de la cuenta en Okta (es posible que ya haya iniciado sesión, así que vaya directamente)
- Okta envía respuesta SAML a Supabase
- Cree una sesión después de la verificación de Supabase y regrese a su aplicación.
El lado del código inicia el inicio de sesión SSO:
//Después de que el usuario ingrese su dirección de correo electrónico, llame a este método
const { data, error } = await supabase.auth.signInWithSSO({
domain: 'acme.com'
})
if (data?.url) {
// Saltar a la página de inicio de sesión SSO
window.location.href = data.url
}
También puedes usar sso_provider_id directamente:
const { data, error } = await supabase.auth.signInWithSSO({
providerId: 'provider_abc'
})
Características de los usuarios de SSO
Los usuarios creados para iniciar sesión en SSO son diferentes de los usuarios normales:
- El correo electrónico es administrado por IdP: los usuarios no pueden cambiar su dirección de correo electrónico en la aplicación
- Sin contraseña: la contraseña se almacena en el IdP, Supabase no participa en la verificación
- No se puede verificar el correo electrónico: porque el IdP ya ha sido verificado
Esto significa que si un usuario necesita cambiar su dirección de correo electrónico, debe pedirle al administrador de TI del cliente que lo haga en Okta/Azure AD.
3. Uso avanzado de la seguridad de nivel de fila
La autenticación resuelve el problema de “quién es el usuario” y la autorización resuelve el problema de “qué puede hacer el usuario”.
El enfoque tradicional es verificar los permisos en el código comercial: cada API debe determinar si el usuario actual puede acceder a estos datos. El código es repetitivo, fácil de pasar por alto y el rendimiento es deficiente: cada solicitud debe consultar la base de datos para determinar los permisos.
La seguridad de nivel de fila (RLS) de PostgreSQL envía comprobaciones de permisos a la capa de base de datos. Cada consulta viene automáticamente con filtrado de permisos, por lo que no hay necesidad de preocuparse por el código comercial.
Comportamiento predeterminado cuando RLS está habilitado
Esto es un problema para muchas personas: después de habilitar RLS, se deniega todo acceso de forma predeterminada.
-- Habilitar RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Cualquier consulta en este momento devuelve nulo (incluidos los administradores)
SELECCIONAR * DE publicaciones; -- devuelve 0 filas
Se debe crear una Política para permitir el acceso.
Dos partes clave de la política
La póliza tiene dos cláusulas: USANDO y CON CHEQUE.
Cláusula USING: Determina si el usuario puede “ver” o “localizar” estos datos. Se utiliza para operaciones SELECCIONAR, ACTUALIZAR, ELIMINAR: el usuario debe poder ver los datos antes de poder modificarlos o eliminarlos.
CON cláusula CHECK: determine si el usuario puede “escribir” estos datos. Se utiliza para operaciones INSERTAR y ACTUALIZAR: los datos escritos deben cumplir las condiciones.
-- Los usuarios sólo pueden operar sus propias publicaciones
CREATE POLICY "Users manage own posts" ON posts
FOR ALL TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Sólo se permite ver (no modificar)
CREATE POLICY "Users view own posts" ON posts
FOR SELECT TO authenticated
USING (auth.uid() = author_id);
-- Sólo se permite insertar (no se pueden ver los de otras personas)
CREATE POLICY "Users insert posts" ON posts
FOR INSERT TO authenticated
WITH CHECK (auth.uid() = author_id);
auth.uid() devuelve el UUID del usuario actualmente conectado. Devuelve nulo si no ha iniciado sesión, por lo que “TO autenticado” garantiza que la Política solo entre en vigor para los usuarios que han iniciado sesión.
RESTRICTIVE vs PERMISSIVE
Una tabla puede tener varias políticas. El valor predeterminado es el modo PERMISIVO: se puede acceder al acceso si se cumple alguna política.
-- Política 1: Los usuarios pueden ver los suyos
CREATE POLICY "Own data" ON posts
FOR SELECT USING (auth.uid() = author_id);
-- Política 2: Todos pueden ver las publicaciones públicas
CREATE POLICY "Public posts" ON posts
FOR SELECT USING (is_public = true);
-- Dos políticas PERMISIVAS: cualquiera de ellas es suficiente
La política RESTRICTIVA es “debe cumplir”. Se utilizará en combinación con la Política PERMISIVA para formar restricciones más estrictas.
-- Todos los accesos deben cumplir esta condición (como "filtro global")
CREATE POLICY "Tenant isolation" ON posts
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
SELECT tenant_id FROM users WHERE id = auth.uid()
));
-- Plus Política PERMISIVA para controlar operaciones específicas
CREATE POLICY "Authors edit own posts" ON posts
FOR UPDATE USING (auth.uid() = author_id);
El efecto de esta combinación: el usuario debe pertenecer al inquilino correcto (RESTRICTIVO) y ser autor para editar (PERMISIVO).
Modo completo de aislamiento multiinquilino
Supongamos que tiene una aplicación SaaS y cada cliente empresarial es un inquilino. La tabla de usuarios tiene “tenant_id” y todas las tablas comerciales deben estar aisladas por inquilino.
--Tabla de usuarios
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
email TEXT,
sso_provider_id TEXTO: los usuarios de SSO solo tienen esto
);
--Mesa de negocios
CREATE TABLE projects (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name TEXT,
created_by UUID REFERENCES users(id)
);
-- Habilitar RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Aislamiento global de inquilinos (RESTRICTIVO)
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
SELECT tenant_id FROM users WHERE id = auth.uid()
));
-- Permisos de operación específicos (PERMISIVO)
CREATE POLICY "Tenant users can view" ON projects
FOR SELECT TO authenticated
USANDO (verdadero); -- ha sido filtrado por RESTRICTIVO y se publicará directamente aquí.
CREATE POLICY "Project creators can edit" ON projects
FOR UPDATE TO authenticated
USING (created_by = auth.uid());
Después de esta configuración, cada consulta realiza automáticamente el filtrado de inquilinos. Incluso si el código comercial escribe “SELECCIONAR * DE proyectos”, la base de datos solo devolverá los datos del inquilino actual.
Aislamiento multiinquilino para usuarios de SSO
Los usuarios que han iniciado sesión en SSO se pueden aislar usando sso_provider_id. Hay un campo en JWT que almacena el método de inicio de sesión:
-- Política RLS para usuarios de SSO
CREATE POLICY "SSO tenant isolation" ON organization_settings
AS RESTRICTIVE TO authenticated
USING (sso_provider_id = (
SELECT auth.jwt()#>>'{amr,0,provider}'
));
auth.jwt()#>>'{amr,0,provider}' Extrae el ID del proveedor del método de inicio de sesión del JWT. Este valor es sso_provider_id durante el inicio de sesión SSO.
Puntos clave de optimización del rendimiento
La política RLS es una cláusula WHERE implícita. Cada consulta se agrega automáticamente y puede contener subconsultas. Esto afecta el rendimiento.
Varias sugerencias de optimización:
1. Cree un índice para los campos en Política
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);
2. Evite subconsultas en Política
La subconsulta se ejecuta una vez en cada fila de datos, lo cual es muy costoso. Un mejor enfoque es almacenar el id_inquilino en el JWT y extraerlo directamente usando auth.jwt():
-- No recomendado: subconsulta
USING (tenant_id = (SELECT tenant_id FROM users WHERE id = auth.uid()))
-- Recomendación: almacenar el id_inquilino en JWT
USING (tenant_id = (auth.jwt()->>'tenant_id')::uuid)
Más adelante hablaremos sobre cómo usar el enlace de token de acceso personalizado para colocar Tenten_id en JWT.
3. Utilice la función SECURITY DEFINER para encapsular lógica compleja
La lógica compleja de la Política se puede encapsular en una función, y la función se establece en SECURITY DEFINER para evitar la ejecución repetida cada vez:
CREATE FUNCTION current_tenant_id() RETURNS UUID
LANGUAGE SQL STABLE SECURITY DEFINER AS $$
SELECT tenant_id FROM users WHERE id = auth.uid();
$$;
--Llamar a la función en la Política
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE USING (tenant_id = current_tenant_id());
ESTABLE significa que el valor de retorno de la función permanece sin cambios en la misma transacción, y PostgreSQL lo optimizará para ejecutarlo solo una vez.
4. Implementación de Reclamaciones Aduaneras y RBAC
De forma predeterminada, JWT solo tiene los campos integrados de Supabase: ID de usuario, correo electrónico, rol (autenticado/anónimo), etc. Pero muchas veces necesita más información: rol de usuario (administrador/moderador), ID de inquilino, lista de permisos.
Supabase proporciona un enlace de token de acceso personalizado, que le permite modificar el contenido del JWT antes de enviarlo.
Por qué se necesitan reclamos personalizados
Varios escenarios típicos:
- Control de permisos RBAC: los roles de usuario se almacenan en JWT y la política RLS determina los permisos según los roles.
- Aislamiento multiinquilino: almacene Tenten_id en JWT para evitar subconsultas en la Política
- Simplifique el tamaño de JWT: el JWT predeterminado contiene muchos campos y la sobrecarga de transmisión en escenarios SSR es alta. Los innecesarios se pueden eliminar.
El JWT de Supabase tiene muchos campos predeterminados (session_id, aal, amr, etc.), que deben incluirse en cada solicitud. Si su aplicación tiene una gran cantidad de páginas SSR, JWT se transmite en cookies y el tamaño afectará el rendimiento.
Implementación de enlace de token de acceso personalizado
Hook es una función PL/pgSQL que se ejecuta antes de que se genere el JWT. Puede agregar, modificar y eliminar reclamaciones.
-- Crear función de gancho
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
claims jsonb;
user_role text;
tenant_id uuid;
BEGIN
- Obtener reclamaciones en caso
claims := event->'claims';
-- Obtener roles de usuario de la tabla user_roles
SELECT role INTO user_role
FROM public.user_roles
WHERE user_id = (event->>'user_id')::uuid;
-- Si el usuario tiene un rol, agréguelo a los reclamos.
IF user_role IS NOT NULL THEN
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
END IF;
-- Obtener id_inquilino de la tabla de usuarios
SELECT tenant_id INTO tenant_id
FROM public.users
WHERE id = (event->>'user_id')::uuid;
IF tenant_id IS NOT NULL THEN
claims := jsonb_set(claims, '{tenant_id}', to_jsonb(tenant_id));
END IF;
-- Devolver reclamaciones modificadas
RETURN jsonb_build_object('claims', claims);
END;
$$;
-- Autoriza a supabase_auth_admin a ejecutar esta función
GRANT EXECUTE ON FUNCTION public.custom_access_token_hook(jsonb)
TO supabase_auth_admin;
-- Habilite este gancho en el panel de Supabase
-- Autenticación > Ganchos > Token de acceso personalizado > Seleccione la función de arriba
Los parámetros de entrada del evento de Hook incluyen:
user_id: UUID del usuario actualreclamaciones: reclamaciones actuales de JWTauthentication_method: método de inicio de sesión (contraseña, oauth, sso/saml, etc.)
Las “reclamaciones” devueltas se fusionarán en el JWT final.
Diseño de estructura de mesa RBAC
Un sistema completo de permisos de roles requiere varias tablas:
-- Definir tipo de rol
CREATE TYPE app_role AS ENUM ('admin', 'moderator', 'user');
-- Definir tipo de permiso
CREATE TYPE app_permission AS ENUM (
'posts.delete', elimina cualquier publicación
'posts.pin', - publicación fijada
'users.manage', -- Administrar usuarios
'settings.edit' - Editar configuración
);
-- Tabla de roles de usuario
CREATE TABLE public.user_roles (
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role app_role NOT NULL,
PRIMARY KEY (user_id, role)
);
-- Tabla de permisos de roles
CREATE TABLE public.role_permissions (
role app_role NOT NULL,
permission app_permission NOT NULL,
PRIMARY KEY (role, permission)
);
-- Insertar permisos predeterminados
INSERT INTO role_permissions (role, permission) VALUES
('admin', 'posts.delete'),
('admin', 'posts.pin'),
('admin', 'users.manage'),
('admin', 'settings.edit'),
('moderator', 'posts.delete'),
('moderator', 'posts.pin');
función de verificación de permisos de autorización ()
Con la tabla de roles y permisos implementada, también se necesita una función para verificar si el usuario tiene un determinado permiso:
CREATE OR REPLACE FUNCTION public.authorize(
requested_permission app_permission
)
RETURNS boolean
LANGUAGE plpgsql
STABLE SECURITY DEFINER
AS $$
DECLARE
user_role app_role;
BEGIN
- Obtener rol de usuario de JWT
SELECT (auth.jwt()->>'user_role')::app_role
INTO user_role;
-- Si no hay ningún rol en el JWT, devuelve falso
IF user_role IS NULL THEN
RETURN false;
END IF;
-- Verifique si el rol tiene este permiso
RETURN EXISTS (
SELECT 1 FROM public.role_permissions
WHERE role = user_role
AND permission = requested_permission
);
END;
$$;
-- Autorizar al rol autenticado
GRANT EXECUTE ON FUNCTION public.authorize(app_permission)
TO authenticated;
Las llamadas a la política RLS autorizan()
Ahora puede verificar los permisos usando authorize() en Política:
-- Sólo el administrador/moderador puede eliminar publicaciones
CREATE POLICY "Role-based delete" ON posts
FOR DELETE TO authenticated
USING (
authorize('posts.delete') OR auth.uid() = author_id
);
--Solo el administrador puede fijar una publicación en la parte superior
CREATE POLICY "Admin pin posts" ON posts
FOR UPDATE TO authenticated
USING (
NOT is_pinned OR authorize('posts.pin')
);
La lógica de la primera política: los usuarios pueden eliminar publicaciones con el permiso posts.delete (administrador/moderador) o como autor de la publicación.
La lógica de la segunda política: al modificar una publicación, si desea establecer “is_pinned” en verdadero, debe tener el permiso “posts.pin”.
El front-end lee reclamos personalizados
Reclamaciones personalizadas en JWT, la interfaz necesita decodificar el token de acceso para leer:
import { jwtDecode } from 'jwt-decode'
// Obtener sesión
const { data: { session } } = await supabase.auth.getSession()
if (session) {
const decoded = jwtDecode(session.access_token)
console.log('Rol de usuario:', decodificado.user_role)
console.log('ID de inquilino:', decodificado.tenant_id)
}
Tenga en cuenta que jwtDecode solo decodifica el JWT y no verifica la firma. No se requiere validación en el front-end; la validación se realiza en el servidor Supabase.
5. Combate real del escenario completo de Enterprise SaaS
Ahora combinamos lo que aprendimos anteriormente para crear una solución empresarial completa de certificación SaaS.
Este es el escenario: su producto SaaS tiene dos tipos de usuarios:
- Usuarios empresariales: inicie sesión a través del SSO de la empresa cliente (Okta/Azure AD) y la cuenta pertenece a un determinado inquilino.
- Usuario individual: inicia sesión a través de Google/GitHub OAuth, no pertenece a ningún inquilino y solo puede acceder a recursos públicos.
Requisitos de aislamiento de datos: los usuarios empresariales solo pueden ver los datos de sus propios inquilinos y los usuarios individuales no pueden ver los datos empresariales.
Diseño de tabla de usuario
CREATE TABLE public.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
-- Fuente de certificación
auth_type TEXTO NO NULO POR DEFECTO 'oauth', -- 'oauth' o 'sso'
-- Campos específicos del usuario de SSO
sso_provider_id TEXTO, -- ID de la conexión SSO
inquilino_id UUID, -- el inquilino al que pertenece
--Información básica
full_name TEXT,
avatar_url TEXT,
-- marca de tiempo
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Tabla de inquilinos
CREATE TABLE public.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
sso_provider_id TEXTO NO NULO ÚNICO, -- asociado con conexiones SSO
plan_type TEXTO NO NULO POR DEFECTO 'equipo', -- 'equipo' o 'empresa'
created_at TIMESTAMPTZ DEFAULT now()
);
Procesamiento unificado de gancho de token de acceso personalizado
Hook necesita distinguir entre dos tipos de usuarios:
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
claims jsonb;
user_record RECORD;
BEGIN
claims := event->'claims';
- Obtener información del usuario
SELECT auth_type, sso_provider_id, tenant_id, role
INTO user_record
FROM public.users
WHERE id = (event->>'user_id')::uuid;
-- Si el usuario no existe (recién registrado), omita
IF user_record IS NULL THEN
RETURN jsonb_build_object('claims', claims);
END IF;
-- Agregar tipo de autenticación
claims := jsonb_set(claims, '{auth_type}', to_jsonb(user_record.auth_type));
--El usuario de SSO agrega inquilino_id y sso_provider_id
IF user_record.auth_type = 'sso' THEN
IF user_record.tenant_id IS NOT NULL THEN
claims := jsonb_set(claims, '{tenant_id}', to_jsonb(user_record.tenant_id));
END IF;
IF user_record.sso_provider_id IS NOT NULL THEN
claims := jsonb_set(claims, '{sso_provider_id}', to_jsonb(user_record.sso_provider_id));
END IF;
END IF;
--Agregar rol de usuario (si corresponde)
IF user_record.role IS NOT NULL THEN
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_record.role));
END IF;
RETURN jsonb_build_object('claims', claims);
END;
$$;
Proceso de creación de usuario después de iniciar sesión
Tanto los inicios de sesión de OAuth como los de SSO activan la creación de registros en la tabla auth.users. Necesitamos sincronizar la información del usuario con la tabla public.users después de iniciar sesión correctamente.
Puede utilizar mfa_verification_hook de Auth Hooks o el código comercial para procesar:
// En la página de devolución de llamada de autenticación, después de iniciar sesión correctamente
const { data: { user } } = await supabase.auth.getUser()
if (user) {
// Comprobar si el usuario ya existe en public.users
const { data: existingUser } = await supabase
.from('users')
.select('id')
.eq('id', user.id)
.single()
if (!existingUser) {
// Determinar el método de inicio de sesión
const authType = user.app_metadata?.provider || 'oauth'
//Crear registro de usuario
await supabase.from('users').insert({
id: user.id,
email: user.email,
auth_type: authType.startsWith('sso') ? 'sso' : 'oauth',
sso_provider_id: user.app_metadata?.sso_provider_id,
inquilino_id: nulo, // será asignado por el administrador más tarde
full_name: user.user_metadata?.full_name,
avatar_url: user.user_metadata?.avatar_url
})
}
}
Política RLS unificada
La tabla de negocios debe tener una política RLS unificada para manejar tanto a los usuarios de OAuth como a los usuarios de SSO:
-- Supongamos que hay una tabla de proyectos.
CREATE TABLE public.projects (
id UUID PRIMARY KEY,
tenant_id UUID REFERENCES tenants(id),
name TEXT NOT NULL,
is_public BOOLEAN DEFAULT false,
created_by UUID REFERENCES users(id)
);
ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
-- Política de aislamiento global (RESTRICTIVO)
CREATE POLICY "Tenant or public access" ON public.projects
AS RESTRICTIVE TO authenticated
USING (
-- Usuario SSO: debe coincidir con el id_inquilino
(auth.jwt()->>'auth_type' = 'sso'
AND tenant_id = (auth.jwt()->>'tenant_id')::uuid)
OR
-- Usuarios de OAuth: solo pueden ver proyectos públicos
(auth.jwt()->>'auth_type' = 'oauth' AND is_public = true)
);
-- Permitir visualización (PERMISIVO)
CREATE POLICY "Users can view" ON public.projects
FOR SELECT TO authenticated
USING (true);
-- Permitir la creación (solo los usuarios inquilinos pueden crear)
CREATE POLICY "Tenant users can create" ON public.projects
FOR INSERT TO authenticated
WITH CHECK (
auth.jwt()->>'auth_type' = 'sso'
AND tenant_id = (auth.jwt()->>'tenant_id')::uuid
);
-- Permitir edición (creador o administrador)
CREATE POLICY "Creators or admins can edit" ON public.projects
FOR UPDATE TO authenticated
USING (
created_by = auth.uid()
OR authorize('projects.edit')
);
La lógica de esta Política:
- Los usuarios de SSO solo pueden ver proyectos de su propio inquilino.
- Los usuarios de OAuth solo pueden ver proyectos públicos
- Solo los usuarios de SSO pueden crear proyectos (deben pertenecer al inquilino)
- Editar permisos: Creador o administrador con permisos
projects.edit
El administrador del cliente empresarial asigna inquilinos
El tenant_id es nulo cuando el usuario de SSO inicia sesión por primera vez. Requiere asignación manual por parte del administrador empresarial:
--El administrador agrega el usuario al inquilino
UPDATE public.users
SET id_inquilino = '<InquilinoUUID>'
DONDE id = '<UserUUID>';
--Establecer rol de usuario
INSERT INTO public.user_roles (user_id, role)
VALORES ('<UserUUID>', 'admin');
Este proceso se puede convertir en una interfaz de administración o manejarse automáticamente mediante enlaces de autenticación (asignados de forma anónima al inquilino según el proveedor de SSO).
Diagrama de proceso completo
Inicio de sesión de usuario
│
├─ Usuario de OAuth
│ │
│ ├─ Devolución de llamada de Google/GitHub
│ ├─ Supabase crea usuarios de autenticación
│ ├─ Creación de código comercial public.users (auth_type='oauth')
│ └─ JWT: { auth_type: 'oauth' }
│ │
│ └─ RLS: Solo se pueden ver los datos con is_public=true
│
└─ Usuarios de SSO
│
├─ Devolución de llamada SAML de Okta/Azure AD
├─ Supabase crea auth.users (con sso_provider_id)
├─ Creación de código comercial public.users (auth_type='sso')
├─ El administrador asigna id_inquilino
├─ Custom Hook agrega inquilino_id a JWT
└─ JWT: { auth_type: 'sso', tenant_id: '...' }
│
└─ RLS: solo se pueden ver los datos que coinciden con el id_inquilino
Comenzamos con el inicio de sesión social OAuth, avanzamos hacia la integración empresarial SAML SSO y finalmente utilizamos RLS y Custom Claims para crear un sistema de permisos completo. Esta solución puede cubrir varios escenarios, desde productos personales hasta SaaS empresarial.
Varios puntos de decisión clave:
Si tus usuarios son principalmente consumidores individuales: OAuth (Google/GitHub) es suficiente. La configuración es sencilla, los usuarios están acostumbrados a ella y el costo de mantenimiento es bajo. RLS Basic, donde los usuarios sólo pueden acceder a sus propios datos, es suficiente.
Si utiliza B2B SaaS: SSO es una característica imprescindible. Los clientes empresariales lo exigirán; de lo contrario, pueden ser eliminados directamente. Listo para la integración de Okta/Azure AD/Google Workspace, listo para múltiples inquilinos.
Si realiza aplicaciones empresariales complejas: la combinación RBAC + RLS es la solución estándar. Tabla de permisos de roles, reclamaciones personalizadas, función de autorización(): esta combinación puede manejar requisitos de permisos precisos, como “los administradores pueden eliminar pero no pueden fijar”, “los miembros inquilinos pueden editar pero no pueden eliminar”.
Sugerencia del siguiente paso: comience con OAuth y siga el proceso básico. Cuando el proyecto madure y haya demanda de los clientes empresariales, se agregarán SSO y RBAC. La arquitectura de Supabase admite actualizaciones incrementales, por lo que no es necesario configurar todas las funciones al principio.
Proceso completo de configuración de nivel empresarial de Supabase Auth
Complete los pasos de configuración desde el inicio de sesión social de OAuth hasta la integración empresarial SAML SSO y el aislamiento de permisos multiinquilino de RLS.
⏱️ Estimated time: 2 hr
- 1
Step 1: Configurar el proveedor de OAuth (Google/GitHub/Apple)
1. Cree una aplicación OAuth en la consola del proveedor (Consola de Google Cloud/Configuración de GitHub)
2. Configure la dirección de devolución de llamada: https://<project ref>.supabase.co/auth/v1/callback
3. Habilite el Proveedor en el Panel de Supabase y complete la ID del cliente y el Secreto
4. El código llama a signInWithOAuth(), especificando alcances y redirigir a - 2
Step 2: Configurar la integración empresarial SAML SSO
1. Obtenga el archivo de metadatos o la URL del IdP empresarial (Okta/Azure AD)
2. Agregue una conexión SSO mediante la CLI: supabase sso add --type saml --domains company.com
3. Configure la asignación de atributos para asignar el correo electrónico, el nombre y otros campos
4. Pruebe el proceso de inicio de sesión: el usuario salta automáticamente al IdP después de ingresar su dirección de correo electrónico - 3
Step 3: Implementar el aislamiento multiinquilino de RLS
1. Agregue el campo inquilino_id a la tabla comercial.
2. Habilite RLS: ALTERAR TABLA proyectos HABILITAR SEGURIDAD DE NIVEL DE FILA
3. Cree una política RESTRINGIDA para implementar el filtrado global de inquilinos.
4. Cree una Política PERMISIVA para controlar permisos de operación específicos (SELECCIONAR/INSERTAR/ACTUALIZAR)
5. Agregue un índice al campo Tenten_id para optimizar el rendimiento. - 4
Step 4: Configurar el enlace de token de acceso personalizado
1. Cree la función public.custom_access_token_hook()
2. Agregue inquilino_id, usuario_role y otros reclamos personalizados en la función.
3. Autorizar a supabase_auth_admin para ejecutar funciones
4. Habilite los ganchos en el panel de Supabase (Autenticación > Ganchos)
5. La interfaz lee reclamos personalizados a través de jwtDecode() - 5
Step 5: Implementar el control de permisos RBAC
1. Cree tablas user_roles y role_permissions
2. Defina los tipos de enumeración app_role y app_permission
3. Cree la función Authorize() para verificar los permisos del usuario.
4. Llame a Authorize('permission.name') en la política RLS
5. La interfaz muestra/oculta elementos de la interfaz de usuario según el rol de usuario en JWT
FAQ
¿Cuál es la diferencia entre OAuth y SAML SSO? ¿Cuál debería elegir?
Si está utilizando B2B SaaS, SSO es una característica imprescindible; si es un producto personal, OAuth es suficiente.
¿Qué debo hacer si la consulta arroja resultados vacíos después de habilitar RLS?
El rendimiento de la política RLS es deficiente, ¿cómo optimizarlo?
• Cree un índice para los campos en Política: CREAR ÍNDICE idx_tenant ON proyectos(tenant_id);
• Evite subconsultas en la Política: use el enlace de token de acceso personalizado para colocar Tenant_id en JWT y extraerlo directamente usando auth.jwt()->>'tenant_id'.
• Utilice la función SECURITY DEFINER para encapsular lógica compleja y PostgreSQL la optimizará para ejecutarla solo una vez.
¿Cómo almacenar campos personalizados (como Tenant_id, user_role) en JWT?
1. Cree la función PL/pgSQL custom_access_token_hook (evento jsonb)
2. Utilice jsonb_set() para agregar reclamos personalizados en la función
3. Habilite el enlace en el panel de Supabase (Autenticación > Ganchos > Token de acceso personalizado)
4. La interfaz usa jwtDecode() para leer reclamos personalizados en JWT
¿Cómo logra SaaS multiinquilino el aislamiento de datos entre usuarios empresariales y usuarios individuales?
• Usuarios de SSO: solo se pueden ver los datos del inquilino que coinciden con el id_inquilino.
• Usuarios de OAuth: solo pueden ver datos públicos con is_public=true
Utilice la Política RESTRICTIVA como filtro global y la Política PERMISIVA para controlar acciones específicas.
¿Supabase admite el inicio de sesión de Apple? ¿Es complicada la configuración?
1. Cree una ID de servicios en el Portal de desarrolladores de Apple
2. Generar clave privada (archivo .p8, solo se puede descargar una vez)
3. Complete el ID del equipo, el ID de la clave, el ID de los servicios y el contenido de la clave privada en el Panel de Supabase.
Las aplicaciones de iOS pueden usar la API nativa de inicio de sesión con Apple, obtener el token de identidad y pasarlo a signInWithIdTokenCredentials() de Supabase.
¿Qué debo hacer si mi cliente empresarial requiere SSO pero no conozco su tipo de IdP?
24 min de lectura · Publicado el: 21 abr 2026 · Actualizado el: 21 ago 2026
Supabase en práctica
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Supabase Edge Functions en la práctica: runtime Deno y guía de desarrollo con TypeScript
Guía completa de Supabase Edge Functions: arquitectura del runtime Deno e isolate V8, flujo de comandos CLI, API RESTful con Hono, del depurado local al despliegue en producción
Parte 8 de 10
Siguiente
Supabase Edge Functions en la práctica: runtime Deno y despliegue global en el borde
Supabase Edge Functions usa el runtime Deno y ejecuta código en nodos edge globales con arranque en frío de 120 ms. Análisis de la arquitectura ESZip, ventajas de Deno, casos prácticos y comparación con Cloudflare Workers.
Parte 10 de 10



Comentarios
Inicia sesión con GitHub para dejar un comentario