Widget OpenWAF VerifyNuevo

Ver en Markdown

Protege un formulario o una llamada a la API con un widget de prueba de trabajo servido desde tu propio host y comprueba su token en tu servidor con siteverify.

En esta página

OpenWAF Verify es un widget que insertas en un formulario. El navegador del visitante resuelve una prueba de trabajo breve y obtiene un token de un solo uso; tu servidor envía ese token a siteverify antes de aceptar el formulario. Usa el mismo motor que el reto de navegador.

Todo es de primera parte: el script, su worker y sus endpoints los sirve el borde de OpenWAF bajo /__waf/ en tu propio nombre con proxy. No hay iframe de terceros ni cookie de terceros, y el widget funciona con Content-Security-Policy: default-src 'self' (sin script ni estilo en línea, sin eval, sin blob:).

Demo en vivo: developers.openwaf.dev/demo/verify/ ejecuta este widget en este mismo sitio, detrás del borde de OpenWAF, con default-src 'self'.

El nombre que muestra el widget tiene que pasar por el proxy de OpenWAF.

Cómo funciona

  1. Crea un widget para tu zona. Obtienes una site key pública (owk_…) y un secreto (ows_…), que se muestra una sola vez.
  2. Añade el script y un contenedor con la site key a tu página.
  3. El widget pide un reto al borde, lo resuelve en un Web Worker y pone el token en un campo oculto owaf-verify-response del formulario.
  4. Tu servidor envía el token y el secreto a siteverify y solo acepta el formulario cuando la respuesta trae "success": true.

Crear un widget

Un widget tiene un nombre (1–64 caracteres), un modo y de 1 a 10 nombres de la zona (el ápice o nombres bajo él, sin comodines) donde se puede mostrar. Una zona puede tener hasta 20 widgets.

Dominio → Seguridad → Verificación → Widgets → Añadir widget. El secreto aparece una vez en un diálogo con botón de copiar: guárdalo antes de cerrarlo.

Añadirlo a tu página

signup.html
<script src="/__waf/challenge/v1/widget.js" async defer></script>

<form action="/signup" method="post">
  <input name="email" type="email" required>
  <div class="owaf-verify" data-sitekey="owk_TU_SITE_KEY" data-action="signup"></div>
  <button type="submit">Darme de alta</button>
</form>

El script busca todos los elementos .owaf-verify y muestra un widget en cada uno. Cuando el navegador ha resuelto el reto, el contenedor tiene <input type="hidden" name="owaf-verify-response" value="v1.…">, que el formulario envía con el resto de campos.

AtributoValoresSignificado
data-sitekeyowk_…Obligatorio. Site key de un widget cuyos nombres incluyen este.
data-action0–32 de A-Z a-z 0-9 _ -Etiqueta que devuelve siteverify, para que tu servidor compruebe que el token se hizo para este formulario.
data-cdatahasta 255 caracteres ASCII imprimiblesDatos tuyos, que devuelve siteverify.
data-callbacknombre de funciónSe llama con el token si todo va bien.
data-error-callbacknombre de funciónSe llama con un código de error cuando el widget falla.
data-expired-callbacknombre de funciónSe llama cuando el token caduca antes de usarse.
data-themeauto (por defecto), light, darkColores de la caja.
data-languageauto (por defecto), en, esIdioma de la caja; auto sigue al navegador.
data-response-field-namenombre de campoNombre del campo oculto. Por defecto owaf-verify-response.
data-refresh-expiredauto (por defecto), manualauto resuelve un reto nuevo antes de que caduque el token y lo anuncia; manual llama en su lugar al callback de caducidad.

API de JavaScript

Carga el script con ?render=explicit para mostrar tú los widgets, y con onload=<función> para que te avise cuando esté listo:

<script src="/__waf/challenge/v1/widget.js?render=explicit&onload=onVerifyReady" async defer></script>
app.js
function onVerifyReady() {
  const id = window.owafVerify.render("#verify", {
    sitekey: "owk_TU_SITE_KEY",
    action: "login",
    callback: (token) => console.log("listo"),
  });
}
MétodoEfecto
owafVerify.render(elementoOSelector, opciones)Muestra un widget y devuelve su id. Las opciones usan los nombres de los atributos sin data- (sitekey, action, cdata, callback…).
owafVerify.execute(id)Empieza el reto. Obligatorio en los widgets invisible.
owafVerify.reset(id)Descarta el token y vuelve a empezar. Llámalo tras un envío fallido: un token vale una vez.
owafVerify.getResponse(id)Devuelve el token actual, o una cadena vacía.
owafVerify.isExpired(id)Si el token actual ha caducado.
owafVerify.remove(id)Quita el widget.

Modos

ModoQué ve el visitanteInteracción
managedUna caja de 300×65 con una línea de estadoSolo cuando el navegador dice que está automatizado (navigator.webdriver): la caja muestra un botón «Soy una persona» con el nombre de tu host y un reto más duro.
non_interactiveUna caja de 300×65 con una línea de estadoNunca. Un navegador automatizado recibe el error interaction-required.
invisibleNadaNunca. Llama a execute(id), por ejemplo al enviar el formulario. Un navegador automatizado recibe interaction-required.

En esta versión la única señal que pide interacción es navigator.webdriver. Las señales de riesgo del lado del servidor llegarán en una versión posterior. La línea de estado es una región viva que leen los lectores de pantalla, y la caja se alcanza con el teclado.

Tokens

  • Un token empieza por v1., tiene como mucho 2048 caracteres y vale 300 segundos.
  • Es de un solo uso: la primera llamada a siteverify lo gasta en todos los nodos de borde y en el plano de control; una segunda recibe timeout-or-duplicate.
  • Va ligado a la red del visitante (/24 IPv4, /64 IPv6) y lleva el nombre del host, la acción y el cdata.

Comprobar el token en tu servidor

Envía el token a siteverify desde tu servidor, nunca desde el navegador. Puedes llamarlo en tu propio host, a través del borde, o en el plano de control:

curl -s https://example.com/__waf/challenge/siteverify \
  -d secret="$OWAF_VERIFY_SECRET" \
  -d response="$TOKEN" \
  -d remoteip="203.0.113.7"
{"success": true, "challenge_ts": "2026-10-28T10:15:03Z", "hostname": "example.com",
 "action": "signup", "error-codes": [], "metadata": {"mode": "managed", "interactive": false, "risk": "low"}}

Comprueba success, después hostname y action. Los campos, los códigos de error, los límites y la idempotencia están en la referencia de siteverify.

Ejemplos

app/api/signup/route.ts
export async function POST(req: Request) {
  const form = await req.formData();
  const res = await fetch("https://dash.openwaf.dev/v1/challenge/siteverify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      secret: process.env.OWAF_VERIFY_SECRET,
      response: form.get("owaf-verify-response"),
      remoteip: req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
    }),
  });
  const result = await res.json();
  if (!result.success || result.action !== "signup") {
    return Response.json({ error: "verification failed" }, { status: 403 });
  }
  // crear la cuenta
  return Response.json({ ok: true });
}

Detrás del borde de OpenWAF, la dirección de cliente que tu origen ve en REMOTE_ADDR es la del borde; toma la del visitante de la cabecera en la que confía tu origen, o no envíes remoteip.

Claves de prueba

Estas claves funcionan en cualquier host con OpenWAF Verify y en los dos endpoints de siteverify. Nunca gastan un contador ni producen un token real.

ClaveComportamiento
owk_test_passSite key: siempre acierta con el token v1.TEST.TOKEN.
owk_test_failSite key: siempre falla con el error test-fail.
owk_test_interactiveSite key: siempre muestra el paso «Soy una persona» y después da v1.TEST.TOKEN.
ows_test_passSecreto: cualquier respuesta no vacía acierta, con metadata.result_with_testing_key: true.
ows_test_failSecreto: siempre invalid-input-response.
ows_test_spentSecreto: siempre timeout-or-duplicate.

Gestionar widgets

TareaPanelCLIAPIMCP
ListarSeguridad → Verificación → Widgetschallenge widgets <zona>GET …/zones/{zone}/challenge/widgetschallenge_widgets action: list
Cambiar nombre, modo o nombresEditarchallenge widget-set <zona> <id> [--name] [--mode] [--hostname…] --reasonPATCH …/challenge/widgets/{widget}action: update
Rotar el secretoRotar secretochallenge rotate-secret <zona> <id> --reasonPOST …/challenge/widgets/{widget}/rotate-secretaction: rotate_secret
BorrarBorrarchallenge widget-rm <zona> <id> --reasonDELETE …/challenge/widgets/{widget}action: delete

Leer necesita un token readonly; los cambios necesitan tech y un motivo. Los widgets no forman parte de la configuración versionada de la zona: los cambios quedan en el registro de auditoría de la organización (challenge.widget.create, update, delete, rotate_secret), nunca con el secreto, y revertir una versión de la zona no los toca. Toda escritura acepta ?dry_run=1.

Rotar el secreto

Rotar devuelve un secreto nuevo una sola vez. El secreto anterior sigue valiendo 24 horas, para que despliegues el nuevo sin verificaciones fallidas. Tras una segunda rotación dentro de esas 24 horas, solo valen los dos secretos más recientes.

Migrar desde Cloudflare Turnstile

Los campos de siteverify y la mayoría de códigos de error se llaman igual, así que migrar es sobre todo renombrar:

TurnstileOpenWAF Verify
https://challenges.cloudflare.com/turnstile/v0/api.js/__waf/challenge/v1/widget.js
class="cf-turnstile"class="owaf-verify"
cf-turnstile-responseowaf-verify-response
window.turnstile.render/execute/reset/getResponse/remove/isExpiredwindow.owafVerify.… (los mismos métodos)
https://challenges.cloudflare.com/turnstile/v0/siteverifyhttps://<tu host>/__waf/challenge/siteverify o https://dash.openwaf.dev/v1/challenge/siteverify
secret, response, remoteip, idempotency_keylos mismos

Después, quita challenges.cloudflare.com de tu Content Security Policy: no se carga nada de otro origen.

Datos tratados

El widget está hecho para comprobar que un navegador hizo el trabajo, no para identificar a la persona:

  • Prefijo de red. El borde usa la /24 IPv4 o la /64 IPv6 del visitante para ligar el reto y el token. Se mantiene en memoria y en el token solo entra un hash.
  • navigator.webdriver. Si el navegador dice que está automatizado.
  • Interacción. Si hubo interacción con la página antes de la comprobación: solo sí o no, nunca movimientos ni teclas.
  • Sin huella digital. Sin canvas, WebGL, audio, fuentes, plugins ni client hints de alta entropía, y sin identificador persistente.
  • Sin cookies. El widget no pone ninguna cookie ni hace peticiones a otro dominio.
  • Dónde. Los retos y los tokens se emiten en nodos de borde de OpenWAF en la UE (Alemania y España).

Las respuestas de siteverify nunca incluyen estas señales. Los recuentos agregados (retos emitidos, resueltos, fallidos; resultados de siteverify) aparecen en la analítica de tu zona.

Última actualización:

↑↓ para moverte↵ para abriresc para cerrar