Puedes crear un área de miembros con cookies firmadas en Webflow Cloud que verifique cada solicitud en el middleware de Next.js, proteja las páginas montadas y mantenga el contenido público del Designer disponible para todos.
Un portal de cliente o una biblioteca de cursos es una solicitud de páginas en el sitio de Webflow que solo se abren para personas que ya conoces. Al mismo tiempo, el equipo de marketing sigue publicando el sitio público en el Designer sin tocar nada de eso.
La pieza que antes requería un host de aplicación independiente ahora se ejecuta en Webflow Cloud, montada en la misma ruta que el sitio y cubierta por el mismo plan de sitio.
La compilación aquí es una aplicación de área de miembros de Next.js montada en /members. Su página de inicio de sesión envía las credenciales a un Route Handler, que emite una cookie de sesión firmada con HMAC. El middleware de borde envía a cualquier persona sin una cookie válida de vuelta a la página de inicio de sesión.
Todo lo criptográfico se ejecuta en Web Crypto porque Webflow Cloud ejecuta la aplicación en Cloudflare Workers, y he implementado manualmente la comparación de tiempo constante por una razón que aparece la primera vez que ejecutas el mismo código localmente y desplegado.
El almacén de credenciales es una lista JSON corta de cuentas guardada en una variable de entorno secreta, lo cual es ideal para un portal con un pequeño conjunto de inicios de sesión que gestionas tú mismo. Un proveedor de identidad debería encargarse del registro y restablecimiento de contraseña de autoservicio. También debería gestionar el inicio de sesión social, y la capa de sesión debería estar diseñada para que puedas añadir un proveedor detrás del mismo middleware más adelante.
¿Qué necesitas para añadir autenticación de usuarios en Webflow?
Necesitas cinco cosas para crear esta área de miembros. Webflow Cloud está disponible desde el plan de sitio Starter gratuito, mientras que montar la aplicación en un dominio personalizado requiere el plan Premium o superior.
Prepara el proyecto, las credenciales de los visitantes, el material de firma, la ruta de montaje y el acceso al entorno antes de empezar:
- Sitio de Webflow: Usa un sitio con soporte para Webflow Cloud y elige una ruta de montaje; cada URL aquí asume
/members. - Proyecto de Next.js: Usa Next.js 15.2.3 o posterior dentro de la versión 15, gestionado con npm. Mantente por debajo de la 16, donde el middleware se convierte en
proxy.tsy no puede ejecutarse en Webflow Cloud; y mantente en la 15.2.3 o superior, porque las versiones anteriores de 15.x contienen la vulnerabilidad GHSA-f82v-jwr5-mffw, una omisión crítica de exactamente la puerta de middleware en la que se basa esta compilación. El comando de andamiaje a continuación se resuelve a la versión 15.x más reciente. - Cuentas de visitantes: Reúne las direcciones de correo electrónico y contraseñas permitidas, luego hashéalas en tu propia máquina antes de almacenar nada.
- Secreto de firma: Genera una cadena larga y aleatoria para firmar las cookies de sesión, y nunca subas ese valor al repositorio.
- Acceso al entorno: Asegúrate de poder gestionar la configuración del entorno de Webflow Cloud del sitio y decide quién conserva el acceso después de la entrega al cliente.
Con estos requisitos previos listos, la construcción depende de configurar la ruta de montaje y los secretos de forma coherente tanto en el desarrollo local como en Webflow Cloud.
6 pasos para añadir autenticación de usuarios en Webflow Cloud
La compilación utiliza seis pasos para crear la aplicación, configurar secretos, firmar sesiones, gestionar el inicio y cierre de sesión, proteger rutas y desplegar una página de inicio de sesión consciente de la ruta de montaje en Webflow Cloud.
Empieza con el proyecto de Next.js, luego traslada la misma cookie y los ajustes de ruta base a través de cada capa de servidor y cliente.
1. Crea la aplicación de Next.js para el área de miembros
Crea un proyecto nuevo de Next.js 15 o superior con npm, la versión y el gestor de paquetes que requiere la documentación de "bring-your-own-app" de Webflow Cloud para Next.js (Astro 6 o 7 y Vite 6.1 o superior son los otros marcos compatibles). El gestor de paquetes es tan importante como la versión.
La misma documentación indica: "Actualmente, Webflow Cloud solo admite el gestor de paquetes npm". Usa npm para el proyecto y genera un package-lock.json.
Prepara la estructura de la aplicación desde el directorio que contendrá el repositorio:
npx create-next-app@15 members --typescript --app --no-src-dir --import-alias "@/*"
cd members
npm run dev
Ahora tienes una aplicación en ejecución en localhost:3000 que muestra la página estándar de Next.js. Webflow Cloud inyecta la ruta base durante el proceso de compilación.
Mantén next.config.ts sin cambios, sin basePath ni output configuraciones, y elige /members como ruta de montaje al configurar el entorno de Webflow Cloud de la aplicación. La página predeterminada debería cargarse en tu navegador y el repositorio debería contener un package-lock.json.
2. Configura las variables de entorno y aplica hash a las cuentas
Configura las tres variables de entorno necesarias, marcando como secretas en el entorno de Webflow Cloud donde se despliega tu aplicación aquellas dos que contienen credenciales o material de firma. Webflow Cloud pone a disposición del proceso de compilación y de la aplicación desplegada en tiempo de ejecución tanto las variables secretas como las que no lo son, ocultando los secretos en los registros de compilación.
Esa disponibilidad durante la compilación permite que Next.js integre NEXT_PUBLIC_BASE_PATH en el paquete del cliente, del cual dependerá posteriormente el formulario de inicio de sesión.
Las tres variables y dónde pertenece cada una:
| Variable | Secreto | Valor |
|---|---|---|
AUTH_SECRET |
Sí | Una cadena aleatoria que firma y verifica las cookies de sesión |
AUTH_USERS |
Sí | Una matriz JSON de objetos { "email", "salt", "hash" }, uno por visitante |
NEXT_PUBLIC_BASE_PATH |
No | /members, la ruta de montaje para que el código del cliente pueda construir las URL correctas |
| Variable → Secreto → Valor |
|---|
AUTH_SECRET |
| Sí |
| Una cadena aleatoria que firma y verifica las cookies de sesión |
AUTH_USERS |
| Sí |
Una matriz JSON de objetos { "email", "salt", "hash" }, uno por visitante |
NEXT_PUBLIC_BASE_PATH |
| No |
/members, la ruta de montaje para que el código del cliente pueda construir las URL correctas |
Esta configuración mantiene las credenciales y el material de firma en secreto, exponiendo únicamente la ruta de montaje al código del cliente.
Generación del secreto de firma
Genera el secreto de firma con un solo comando:
openssl rand -base64 32
El comando devuelve el valor aleatorio que almacenarás como AUTH_SECRET.
Guarda esto como scripts/hash-password.mjs y ejecútalo una vez por cada cuenta:
// Usage: node scripts/hash-password.mjs ana@example.com "her password"
const [email, password] = process.argv.slice(2);
if (!email || !password) {
console.error('Usage: node scripts/hash-password.mjs <email> <password>');
process.exit(1);
}
const encoder = new TextEncoder();
const toBase64Url = (bytes) => Buffer.from(bytes).toString('base64url');
const salt = crypto.getRandomValues(new Uint8Array(16));
const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits(
{ name: 'PBKDF2', hash: 'SHA-256', salt, iterations: 100000 },
keyMaterial,
256,
);
console.log(JSON.stringify({ email, salt: toBase64Url(salt), hash: toBase64Url(new Uint8Array(bits)) }));
Cada ejecución imprime un objeto JSON. Recógelos en una matriz y pega la matriz como el valor de AUTH_USERS. Mantener las cuentas aquí conlleva dos limitaciones. Cada cambio en la lista, ya sea añadir o eliminar a una persona, requiere un nuevo despliegue para que surta efecto, y la cookie existente de una persona eliminada seguirá funcionando hasta que caduque.
El tamaño de una sola variable limita la lista: Cloudflare restringe cada una a 5 KB, lo que, a unos 130 bytes por registro, equivale a unas 40 cuentas.
Hashing de contraseñas de usuario
Webflow no publica su propio límite por valor, así que considera 40 como el tope operativo. Ejecuta el script con un espacio inicial o borra la línea del historial de tu terminal después, ya que la contraseña se escribe en la línea de comandos. El script se ejecuta en tu máquina, donde puede usar el de Node libremente.Buffer
Para el desarrollo local, refleja las variables en :.env.local
AUTH_SECRET=paste-the-openssl-output-here
AUTH_USERS='[{"email":"ana@example.com","salt":"...","hash":"..."}]'
NEXT_PUBLIC_BASE_PATH=
Deja vacío localmente, porque sirve la aplicación en la raíz. Añade a si el andamiaje aún no lo ha hecho.NEXT_PUBLIC_BASE_PATHnext dev.env.local.gitignore
Cuando termines, el entorno de Webflow Cloud listará las tres variables con y marcadas como secretas, y esos secretos se ocultarán de los registros de compilación. también debería analizarse como JSON válido al pegarlo en una consola.AUTH_SECRETAUTH_USERSAUTH_USERS
3. Escribe el asistente de sesión
Escribe un asistente de sesión compartido que una una carga útil base64url de a una firma HMAC-SHA-256 sobre dicha carga. Una cookie firmada y sin estado permite al middleware verificar a un visitante sin realizar una llamada a la base de datos en cada solicitud, manteniendo el acceso económico en el entorno de ejecución de Workers.{ email, exp }
Mantén la lógica de firma, verificación, hashing y comparación en un solo archivo que importen tanto los manejadores de rutas como el middleware.
crypto.subtlees la API estándar de Web Crypto, presente tanto en el entorno de ejecución de Workers como en , por lo que el mismo archivo se comporta de forma idéntica en ambos lugares.next dev
El asistente no estándar es donde ambos divergen. Workers añade , una extensión de Cloudflare que no tiene. El propio de Node reside en , y el objeto global no tiene tal método en ninguno de los dos sitios. Por eso uso un bucle XOR simple que se comporta igual en todas partes.crypto.subtle.timingSafeEqualnext devtimingSafeEqualnode:cryptocrypto
Creando lib/session.ts
Crea con todo lo que comparten la ruta de inicio de sesión y el middleware:lib/session.ts
export const SESSION_COOKIE = 'members_session';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export function toBase64Url(bytes: Uint8Array): string {
let binary = '';
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
export function fromBase64Url(value: string) {
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0));
}
export function constantTimeEqual(expected: Uint8Array, actual: Uint8Array): boolean {
let diff = expected.length ^ actual.length;
for (let i = 0; i < expected.length; i++) {
diff |= expected[i] ^ (actual[i] ?? 0);
}
return diff === 0;
}
export async function hashPassword(password: string, salt: string): Promise<string> {
const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits(
{ name: 'PBKDF2', hash: 'SHA-256', salt: fromBase64Url(salt), iterations: 100000 },
keyMaterial,
256,
);
return toBase64Url(new Uint8Array(bits));
}
async function signingKey(secret: string): Promise<CryptoKey> {
return crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
}
export async function createSessionToken(email: string, secret: string, ttlSeconds: number): Promise<string> {
const exp = Math.floor(Date.now() / 1000) + ttlSeconds;
const payload = toBase64Url(encoder.encode(JSON.stringify({ email, exp })));
const signature = await crypto.subtle.sign('HMAC', await signingKey(secret), encoder.encode(payload));
return `${payload}.${toBase64Url(new Uint8Array(signature))}`;
}
export async function verifySessionToken(
token: string | undefined,
secret: string,
): Promise<{ email: string } | null> {
if (!token || !secret) return null;
const [payload, signature] = token.split('.');
if (!payload || !signature) return null;
try {
const expected = await crypto.subtle.sign('HMAC', await signingKey(secret), encoder.encode(payload));
if (!constantTimeEqual(new Uint8Array(expected), fromBase64Url(signature))) return null;
const data = JSON.parse(decoder.decode(fromBase64Url(payload))) as { email: string; exp: number };
return data.exp > Math.floor(Date.now() / 1000) ? { email: data.email } : null;
} catch {
return null;
}
}
El archivo se compila sin importaciones porque cada API que toca es un elemento global web. devuelve ante un token faltante, una firma incorrecta, una carga útil expirada o un base64 mal formado, por lo que los llamadores solo tienen que bifurcarse ante una única condición.verifySessionTokennull
La comparación recorre el valor esperado de confianza incluso cuando el valor no confiable tiene una longitud diferente. Incorpora esa diferencia de longitud al resultado y completa el bucle sobre el valor esperado. Deberías poder ejecutar y no ver errores en este archivo.npx tsc --noEmit
4. Añade los manejadores de rutas de inicio y cierre de sesión
Añade manejadores de rutas para inicio y cierre de sesión que vuelvan a derivar el hash PBKDF2 de cada contraseña enviada y la sal almacenada antes de compararlos en tiempo constante. El manejador primero busca la cuenta por correo electrónico y establece la cookie si tiene éxito.POST
Cuando el correo electrónico es desconocido, el manejador sigue ejecutando PBKDF2 contra una sal ficticia fija, por lo que los intentos de inicio de sesión tardan un tiempo comparable tanto para direcciones conocidas como desconocidas.
Deja que el adaptador de OpenNext para Cloudflare compile ambos manejadores para el entorno de ejecución de Workers. Añadir a cualquiera de los manejadores selecciona un objetivo de tiempo de ejecución de Next.js que el adaptador no admite y rompe la compilación.export const runtime = 'edge'
El número de iteraciones merece su propia mención, porque 100.000 no es una elección arbitraria. Es el máximo que acepta el entorno de ejecución de producción de Workers: por encima de eso, lanza el error "PBKDF2 failed: iteration counts above 100000 are not supported" y cada inicio de sesión devuelve un 500.deriveBits
Tiempo de ejecución del gestor de rutas e iteraciones PBKDF2
OWASP recomienda 600.000 para PBKDF2-HMAC-SHA256, así que esta es una brecha conocida y la razón por la que un proveedor de identidad alojado es mejor para gestionar contraseñas a gran escala. No aumentes el número para intentar cerrarla. Ni next dev ni el entorno local wrangler dev aplican el límite, por lo que un conteo más alto pasa todas las pruebas locales, pero falla al desplegarse; además, cambiarlo más tarde invalidaría todos los hashes almacenados.
Cada derivación cuenta para los límites de Webflow Cloud, que son fijos en lugar de escalonados por plan: 30 segundos de CPU de Worker por solicitud.
Coloca el gestor de inicio de sesión en app/api/login/route.ts:
import { NextResponse } from 'next/server';
import { SESSION_COOKIE, constantTimeEqual, createSessionToken, hashPassword } from '@/lib/session';
type StoredUser = { email: string; salt: string; hash: string };
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
const DUMMY_SALT = 'AAAAAAAAAAAAAAAAAAAAAA'; // 16 zero bytes, base64url, for unknown emails
export async function POST(request: Request) {
// A cross-site HTML form cannot send a JSON body, so requiring one blocks login CSRF.
if (!request.headers.get('content-type')?.startsWith('application/json')) {
return NextResponse.json({ error: 'Expected a JSON body.' }, { status: 415 });
}
const body = (await request.json().catch(() => ({}))) as { email?: unknown; password?: unknown };
if (typeof body.email !== 'string' || typeof body.password !== 'string' || !body.email || !body.password) {
return NextResponse.json({ error: 'Email and password are required.' }, { status: 400 });
}
const secret = process.env.AUTH_SECRET;
if (!secret) {
return NextResponse.json({ error: 'Server is not configured.' }, { status: 500 });
}
const email = body.email;
const users = JSON.parse(process.env.AUTH_USERS ?? '[]') as StoredUser[];
const user = users.find((entry) => entry.email.toLowerCase() === email.toLowerCase());
const candidate = await hashPassword(body.password, user?.salt ?? DUMMY_SALT);
const encoder = new TextEncoder();
if (!user || !constantTimeEqual(encoder.encode(candidate), encoder.encode(user.hash))) {
return NextResponse.json({ error: 'Invalid email or password.' }, { status: 401 });
}
const token = await createSessionToken(user.email, secret, SESSION_TTL_SECONDS);
const response = NextResponse.json({ ok: true });
response.cookies.set(SESSION_COOKIE, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: process.env.NEXT_PUBLIC_BASE_PATH || '/',
maxAge: SESSION_TTL_SECONDS,
});
return response;
}
La path de la cookie coincide con la ruta de montaje, por lo que el navegador solo la envía para solicitudes bajo /members. Las páginas creadas en el Designer en la raíz del sitio no la reciben. Localmente, la variable está vacía y la ruta vuelve a /.
El gestor de cierre de sesión en app/api/logout/route.ts borra la misma cookie:
import { NextResponse } from 'next/server';
import { SESSION_COOKIE } from '@/lib/session';
export async function POST() {
const response = NextResponse.json({ ok: true });
response.cookies.set(SESSION_COOKIE, '', {
path: process.env.NEXT_PUBLIC_BASE_PATH || '/',
maxAge: 0,
});
return response;
}
El endpoint de inicio de sesión no está autenticado a propósito porque es el que crea la sesión. Añade limitación de tasa o bloqueo de cuenta antes de pasar a producción para restringir el adivinado de contraseñas y el coste de CPU del trabajo repetido de PBKDF2. El endpoint de cierre de sesión solo actúa sobre la cookie de sesión del usuario.
La comprobación JSON al inicio del gestor de inicio de sesión es su defensa contra CSRF. SameSite: 'lax' no hace nada en este endpoint, porque una solicitud de inicio de sesión aún no lleva cookie de sesión; sin esta comprobación, un formulario en otro sitio podría enviar un cuerpo de texto plano y conectar a un visitante a una cuenta controlada por el atacante.
El gestor de cierre de sesión no tiene esta comprobación, por lo que una solicitud entre sitios puede cerrar la sesión de un miembro, lo cual es más una molestia que una brecha de seguridad.
Consideraciones de seguridad
La fuga de variables de entorno visibles para el cliente se limita a NEXT_PUBLIC_BASE_PATH, que es intencionadamente pública y solo contiene /members. AUTH_SECRET y AUTH_USERS no tienen el prefijo público y solo son leídas por el código del lado del servidor.
Con next dev en ejecución, una curl -i -X POST localhost:3000/api/login -H 'Content-Type: application/json' -d '{"email":"ana@example.com","password":"her password"}' devuelve 200 con una cabecera Set-Cookie: members_session=...; una contraseña incorrecta devuelve 401 aproximadamente en el mismo tiempo.
5. Protege la aplicación con middleware.ts
Crea el archivo middleware en la raíz del proyecto junto al directorio app, donde puede proteger todas las rutas excepto la página de inicio de sesión y la API. La documentación de personalización del framework de Webflow indica: "Solo el middleware de tiempo de ejecución Edge funciona en el tiempo de ejecución de Workers".
Next.js ejecuta middleware.ts en su tiempo de ejecución Edge por defecto, que es exactamente lo que acepta el tiempo de ejecución de Workers, y lo hace sin ninguna exportación runtime.
Next.js 16 renombra el middleware a proxy, y proxy.ts se ejecuta en el tiempo de ejecución de Node sin posibilidad de optar por Edge, razón por la cual esta compilación se mantiene en Next.js 15 y conserva el archivo como middleware.ts.
Construye la redirección desde request.nextUrl.clone(). NextURL lleva consigo la ruta base inyectada, por lo que el visitante aterriza en /members/login. Una redirección construida desde new URL('/login', request.url) apuntaría a una /login inexistente en la raíz del sitio.
Crea middleware.ts en la raíz del proyecto:
import { NextResponse, type NextRequest } from 'next/server';
import { SESSION_COOKIE, verifySessionToken } from '@/lib/session';
export async function middleware(request: NextRequest) {
const token = request.cookies.get(SESSION_COOKIE)?.value;
const session = await verifySessionToken(token, process.env.AUTH_SECRET ?? '');
if (session) {
return NextResponse.next();
}
const loginUrl = request.nextUrl.clone();
loginUrl.pathname = '/login';
return NextResponse.redirect(loginUrl);
}
export const config = {
matcher: ['/((?!login$|api/login$|api/logout$|_next/|favicon\\.ico$).*)'],
};
El comparador es una búsqueda negativa que omite exactamente cinco cosas: la página /login, las rutas de inicio y cierre de sesión, los activos de Next.js bajo /_next/, y el favicon. Los delimitadores son importantes. Sin el $ y las barras inclinadas finales, la búsqueda omitiría cualquier ruta que simplemente comience con esas letras, por lo que una página de miembros en /api-guide o /login-history sería visible para cualquiera.
Nombrar las dos rutas abiertas, en lugar de omitir todo /api/, también significa que cualquier ruta de API que añadas más tarde estará protegida por defecto, no pública. Cargar localhost:3000 en una ventana privada ahora redirige a localhost:3000/login, que devuelve un 404 hasta que la página de inicio de sesión exista; ese 404 confirma que el middleware se activó.
6. Crea la página de inicio de sesión y despliégala
Crea una página de inicio protegida que lea la sesión en el servidor y un formulario de inicio de sesión renderizado en el cliente que añada el prefijo de la ruta de montaje a cada .fetch
Ese prefijo es el detalle que todo el mundo pasa por alto. Next.js monta automáticamente los controladores de rutas del lado del servidor en la ruta base inyectada, y la documentación de "bring-your-own-app" es clara al señalar que "las llamadas fetch del lado del cliente deben incluir manualmente la ruta base para llegar correctamente a tus endpoints".
Un formulario que hace un POST a funciona bajo . Una vez desplegado, esa solicitud se dirige fuera de la aplicación montada en lugar de llegar a ./api/loginnext dev/members/api/login
Reemplaza con una página de inicio protegida que lea la sesión:app/page.tsx
import { cookies } from 'next/headers';
import { SESSION_COOKIE, verifySessionToken } from '@/lib/session';
export default async function MembersHome() {
const cookieStore = await cookies();
const session = await verifySessionToken(
cookieStore.get(SESSION_COOKIE)?.value,
process.env.AUTH_SECRET ?? '',
);
return (
<main>
<h1>Members area</h1>
<p>Signed in as {session?.email ?? 'unknown'}</p>
</main>
);
}
El middleware ya ha rechazado a los visitantes anónimos para cuando esto se renderiza, así que el fallback de solo existe para mantener la coherencia con TypeScript.'unknown'
Creación del componente de la página de inicio de sesión
El formulario de inicio de sesión va en como un componente de cliente:app/login/page.tsx
'use client';
import { useState, type FormEvent } from 'react';
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
export default function LoginPage() {
const [error, setError] = useState<string | null>(null);
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
const data = new FormData(event.currentTarget);
const response = await fetch(`${basePath}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: data.get('email'), password: data.get('password') }),
});
if (!response.ok) {
setError('That email and password did not match.');
return;
}
window.location.assign(basePath || '/');
}
return (
<main>
<h1>Sign in</h1>
<form onSubmit={handleSubmit}>
<label>
Email
<input name="email" type="email" autoComplete="email" required />
</label>
<label>
Password
<input name="password" type="password" autoComplete="current-password" required />
</label>
<button type="submit">Sign in</button>
</form>
{error && <p role="alert">{error}</p>}
</main>
);
}
Despliega la aplicación en el entorno de Webflow Cloud montado en , con las tres variables de entorno configuradas antes de que se ejecute la compilación para que se incluya en línea./membersNEXT_PUBLIC_BASE_PATH
Abre en una ventana privada. Se te redirigirá a , una contraseña correcta te enviará de vuelta a mostrando "Signed in as" con tu correo electrónico, y las cabeceras de respuesta en la solicitud de inicio de sesión mostrarán con , , y .yourdomain.com/members/members/login/membersSet-CookieHttpOnlySecurePath=/members
¿Qué causa que la autenticación de usuario falle en Webflow Cloud?
La autenticación suele fallar debido a una ruta base de montaje incorrecta, un tiempo de ejecución de controlador de ruta no compatible, un archivo de middleware renombrado o un asistente criptográfico que se comporta de forma distinta durante el desarrollo local.
Empieza por la solicitud fallida en la pestaña Red del navegador o en el registro de compilación correspondiente, y luego compara el síntoma con estas comprobaciones de Webflow Cloud.
El inicio de sesión devuelve un 404 en el sitio en vivo pero funciona en next dev
Causa: Un POST a en la pestaña Red confirma que el paquete desplegado se compiló sin . Dependiendo de la ruta raíz del sitio, esa solicitud puede aparecer como un 404, y recargar la página no puede cambiar el valor incluido en línea. Webflow Cloud sirve la aplicación bajo su ruta de montaje, pero las solicitudes del lado del cliente no obtienen ese prefijo automáticamente./api/login/members
Solución: Configura en antes de la compilación y activa una nueva; editar la variable por sí sola no cambia nada en el paquete enviado. Confirma en la pestaña Red de las herramientas de desarrollo que el POST va a y que la cabecera dice .NEXT_PUBLIC_BASE_PATH/members/members/api/loginSet-CookiePath=/members
He visto que la parte de la cookie falla por sí sola: el fetch es correcto, pero la ruta de la cookie es , lo cual sigue funcionando, y solo se manifiesta como una fuga cuando la cookie empieza a viajar en cada solicitud de página pública./
La compilación falla tras añadir export const runtime = 'edge' a un controlador de ruta
Causa: El controlador de ruta contiene , que selecciona un objetivo de tiempo de ejecución de Next.js que el adaptador OpenNext de Cloudflare no admite. La página de "bring-your-own-app" te indica que lo añadas a las rutas de API, por lo que un desarrollador que siga esa instrucción puede pensar razonablemente que el problema es de la plataforma.export const runtime = 'edge'
El middleware es diferente porque Next.js ya ejecuta en su tiempo de ejecución Edge por defecto.middleware.ts
Solución: Elimina la línea de cada Route Handler y página. Si el proyecto proviene de un host que requería la directiva, busca runtime = 'edge' en el repositorio y elimina cada aparición.
Deja el archivo middleware.ts de nivel raíz sin una exportación runtime. Vuelve a compilar después de la búsqueda para que el adaptador compile los controladores de inicio y cierre de sesión para el entorno de ejecución de Workers, mientras que el middleware mantiene el comportamiento de ejecución que acepta Webflow Cloud.
El control de acceso deja de funcionar tras renombrar middleware.ts a proxy.ts
Causa: Es posible que un codemod de actualización de Next.js haya cambiado el nombre del archivo raíz sin modificar el código interno. El servidor de desarrollo local puede seguir funcionando igual, pero proxy.ts se ejecuta en el entorno de Node sin posibilidad de optar por Edge.
El entorno de ejecución de Workers de Webflow Cloud requiere el comportamiento Edge proporcionado por middleware.ts, por lo que el control de acceso desplegado deja de funcionar y falla permitiendo el paso. Todas las páginas de miembros se sirven sin inicio de sesión y sin mostrar ningún error.
Solución: Restaura el nombre del archivo a middleware.ts y confirma que la exportación sigue llamándose middleware con el config.matcher intacto. Verifica en el sitio desplegado: abre yourdomain.com/members en una ventana privada y comprueba si redirige a /members/login.
Los desarrolladores de agencias que entreguen un sitio deben dejar un comentario en la parte superior del archivo explicando por qué debe conservar su nombre, ya que la próxima actualización de Next.js volverá a sugerir el cambio.
crypto.subtle.timingSafeEqual no es una función en next dev, pero funciona bien en la aplicación desplegada
Causa: crypto.subtle.timingSafeEqual es una extensión de Cloudflare para Web Crypto que existe en el entorno de ejecución de Workers desplegado, mientras que el Web Crypto de Node en next dev carece de ella. Por lo tanto, sustituir ese asistente integrado puede resultar en un despliegue exitoso pero con errores en la terminal.
Un respaldo en tiempo de ejecución también puede hacer que la producción ejecute código de comparación que nunca se probó localmente, lo que dificulta verificar el comportamiento de manera consistente en ambos entornos.
Solución: Restaura el asistente constantTimeEqual en lib/session.ts para que las comparaciones locales y desplegadas sigan la misma ruta de código. Evita crear bifurcaciones basadas en typeof crypto.subtle.timingSafeEqual.
La misma comprobación específica del entorno de ejecución se aplica a los módulos de Node: node:path está disponible en Webflow Cloud, mientras que node:fs no lo está. Comprueba cada módulo con la documentación de Workers y Node local en lugar de inferir la compatibilidad a partir de una compilación exitosa.
Qué puedes construir a continuación con la autenticación de usuarios y Webflow Cloud
Una vez que un correo electrónico verificado viaja con cada solicitud bajo /members, el contenido restringido se convierte en un problema de renderizado común. Un componente de servidor puede leer la sesión, buscar a qué tiene derecho ese visitante y extraer los elementos de colección correspondientes del CMS de Webflow. Puede usar la API REST, de modo que el equipo de marketing siga editando los artículos exclusivos para miembros en el mismo lugar que los públicos.
Sin embargo, no asignes ninguna página de plantilla pública a esas colecciones: una página de colección publicada en el sitio del Designer queda fuera de /members, y el control de acceso nunca la detecta. Los precios por cuenta y la página de perfil comparten la misma cookie. Los enlaces de descarga pueden caducar con la sesión.
Cuando la lista de cuentas supera una variable de entorno, el Route Handler de inicio de sesión se convierte en la devolución de llamada para un proveedor de identidad que devuelve un correo electrónico verificado, como Auth0 o Supabase Auth, y createSessionToken convierte ese correo en la misma cookie en la que ya confía el middleware.
Preguntas frecuentes
¿Cómo puedo revocar todas las sesiones activas?
Reemplaza AUTH_SECRET en el entorno y vuelve a desplegar. La revocación ocurre cuando el nuevo despliegue entra en funcionamiento, no cuando guardas la variable. Cada cookie existente lleva una firma creada con el valor anterior, por lo que la verificación devuelve null y el middleware redirige a cada visitante al inicio de sesión. Usa esto cuando necesites una revocación general y mantén el reemplazo secreto fuera del repositorio.
¿Puedo revocar el acceso de una cuenta de forma inmediata?
No con la comprobación sin estado actual. El middleware solo verifica la firma y la caducidad de la cookie, por lo que eliminar una entrada de AUTH_USERS no invalida una cookie que ya se ha emitido. La revocación inmediata por cuenta requiere una cuenta adicional en el servidor o una comprobación de revocación; de lo contrario, rota AUTH_SECRET para invalidar todas las sesiones activas a la vez.
¿Las direcciones de correo electrónico distinguen entre mayúsculas y minúsculas al iniciar sesión?
No. El gestor de inicio de sesión convierte a minúsculas tanto la dirección enviada como cada dirección almacenada durante la búsqueda. Tras encontrar una coincidencia, inserta la grafía del correo electrónico de {{wf site="site"}} en el token de sesión. Como resultado, un visitante puede variar las mayúsculas al iniciar sesión, mientras que la página de miembros muestra siempre la dirección canónica almacenada.AUTH_USERS
¿Cómo añado un botón de cierre de sesión al área de miembros?
La página de inicio de sesión proporcionada no incluye un control de cierre de sesión, pero el gestor de rutas de cierre de sesión está listo para usarse. Añade un botón en el lado del cliente que envíe una solicitud {{wf method="POST"}} a {{wf path="/api/logout"}}, y luego redirige al visitante a {{wf path="/"}}. El gestor borra la cookie usando la misma ruta de montaje que el inicio de sesión tras cerrar la sesión.POST${basePath}/api/logout${basePath}/login
¿Qué ocurre si AUTH_USERS contiene correos electrónicos duplicados?
El gestor utiliza {{wf method="Array.prototype.find()"}} con una comparación que no distingue entre mayúsculas y minúsculas, por lo que el primer objeto coincidente en {{wf variable="AUTH_USERS"}} es el que prevalece. Por tanto, dos entradas cuyas direcciones solo difieran en el uso de mayúsculas son ambiguas, y las entradas posteriores nunca serán seleccionadas. Asegúrate de que cada correo sea único tras convertirlo a minúsculas y regenera el array JSON si descubres un duplicado.findAUTH_USERS





