Postman + app real: cómo resolver un 400 en Customer List usando CSRF dinámico

Reproducir un flujo real en Postman no es solo copiar requests. Cómo resolver un 400 Bad Request causado por un CSRF token estático, usando variables dinámicas y pre-request scripts.

Postman Collection Runner showing GET Login Page 200, POST Login 400, and POST Customer List 400 before implementing dynamic CSRF token
El punto de partida: Login Page pasa con 200, pero Login y Customer List devuelven 400. Sin CSRF dinámico, el flujo se rompe.

Después de leer la página de login en DevTools (https://demo.serenity.is/Account/Login/?ReturnUrl=%2F) y analizar el POST de autenticación, el siguiente paso lógico era reproducir ese flujo en Postman.

No fue tan directo como parecía.

La colección armada

Para este punto, la colección en Postman ya tenía tres requests:

  1. GET - Login Page
  2. POST - Login
  3. POST - Customer List

La idea era que corrieran en serie: la primera trae la página de login y deja cookies iniciales, la segunda envía usuario y contraseña, la tercera pide los datos de la grilla de clientes.

Las requests se importaron desde Chrome DevTools usando Copy as cURL (bash) y después se simplificaron manualmente dentro de Postman.

Ese detalle importa: importar desde DevTools sirve para traer una versión muy fiel de lo que hace el navegador, pero después hay que entender qué parte es realmente necesaria y cuál es ruido.

Chrome DevTools and Postman side by side showing GET Login Page request with 200 OK status and matching response headers
DevTools y Postman lado a lado: el GET de la página de login devuelve 200 en ambos, con los mismos headers y la cookie CSRF-TOKEN visible.

El síntoma: la tercera request fallaba

Al correr la colección, el flujo no era estable:

Request Resultado
GET - Login Page 200
POST - Login 200 (a veces)
POST - Customer List 400 Bad Request
Postman Collection Runner showing GET Login Page 200, POST Login 200, and POST Customer List returning 400 Bad Request
El síntoma: las dos primeras requests pasan con 200, pero Customer List devuelve 400. El token CSRF estático es el culpable.

Y algo todavía más raro: si se volvía a ejecutar GET - Login Page, el login podía volver a fallar, a menos que se actualizara manualmente cierto valor.

Postman POST Login request showing headers with hardcoded x-csrf-token value
El problema en acción: el header x-csrf-token tiene un valor fijo copiado del navegador. Cuando ese token expira, la request falla.

Eso apuntaba a que el problema no estaba en la URL ni en el body principal, sino en algún dato de seguridad que la app esperaba fresco en cada ciclo.

La pista real: el token CSRF cambiaba

En los pasos anteriores:

Customer/List en DevTools: dónde salen datos de grilla
La grilla de clientes no vive en la UI: los datos vienen de un endpoint separado. Análisis del request Customer/List, su payload con filtros y columnas, y la response JSON con TotalCount.

ya se había visto que la app trabaja con cookies como CSRF-TOKEN, .AspNetCore.Antiforgery y AspNetAuth. Y que en el login real del navegador aparecía un header x-csrf-token.

Postman cookie jar showing three cookies for demo.serenity.is: AspNetAuth, AntiForgery and CSRF-TOKEN
La cookie jar de Postman con las tres cookies del dominio: AspNetAuth, AntiForgery y CSRF-TOKEN. De acá se lee el token dinámicamente.

Lo clave fue notar esto: cada vez que se vuelve a pedir la página de login, el valor de CSRF-TOKEN cambia.

El error de fondo era tratar el token como si fuera un valor copiable y semi-fijo. No lo es. Es un valor dinámico, generado por la app en cada ciclo.

Entonces el problema no era "cómo copiar la request". El problema era: cómo hacer que Postman deje de depender de un token viejo y use siempre el token actual.

Por qué la request "correcta" fallaba

La request tenía todo aparentemente bien: URL correcta, método correcto, body correcto, cookies presentes. Pero eso no alcanzaba.

La app esperaba que el request llevara un valor de CSRF coherente con la cookie actual. Si el valor del header quedaba viejo o no se actualizaba con cada ejecución, la request se rompía.

La solución: convertir el CSRF en una variable dinámica

En vez de dejar un valor hardcodeado, el token pasa a una variable que Postman refresca automáticamente.

El flujo queda así:

  1. GET - Login Page actualiza la cookie jar de Postman
  2. POST - Login lee el valor actual de CSRF-TOKEN desde esa cookie jar
  3. Ese valor se guarda en una variable
  4. El header x-csrf-token usa esa variable
  5. POST - Customer List también refresca el mismo token antes de ejecutarse

Ahí deja de ser una request "estática" y pasa a ser un flujo real.

Implementación en POST - Login

x-csrf-token: {{csrfToken}}

En vez de pegar un token fijo, se usa la variable csrfToken.

Postman POST Login request with x-csrf-token header using dynamic variable csrfToken instead of hardcoded value
La solución: el header x-csrf-token ahora usa la variable {{csrfToken}} en vez de un valor fijo. Se actualiza automáticamente en cada ejecución.

Script en Pre-request

const csrfCookie = pm.cookies.get('CSRF-TOKEN');

if (csrfCookie) {
    pm.variables.set('csrfToken', csrfCookie);
    console.log('csrfToken actualizado desde cookie jar');
} else {
    console.log('No se encontró la cookie CSRF-TOKEN');
}
Postman pre-request script for POST Login reading CSRF-TOKEN from cookie jar and setting csrfToken variable
El pre-request script que cierra el circuito: lee CSRF-TOKEN desde la cookie jar y lo guarda en la variable csrfToken antes de cada ejecución.

La lógica es simple: Postman busca la cookie CSRF-TOKEN, si la encuentra guarda su valor en la variable csrfToken, y después el header usa ese valor actualizado.

Con eso, POST - Login ya no depende de copiar el token a mano.

Pero todavía faltaba una pieza

Con el login resuelto, POST - Customer List seguía fallando.

Esa request también necesita trabajar con un token actualizado. No alcanza con que el login lo haga bien. La request de la grilla también debe resolver el valor antes de ejecutarse.

Header

x-csrf-token: {{csrfToken}}

Script en Pre-request

const csrfCookie = pm.cookies.get('CSRF-TOKEN');

if (csrfCookie) {
    pm.variables.set('csrfToken', csrfCookie);
    console.log('csrfToken actualizado en Customer List desde cookie jar');
} else {
    console.log('No se encontró CSRF-TOKEN en Customer List');
}
Postman pre-request script for POST Customer List reading CSRF-TOKEN from cookie jar with same dynamic token logic
Customer List necesita el mismo tratamiento: su propio pre-request script que refresca el token antes de ejecutarse.

Ese fue el cambio que cerró el circuito. Una vez que Customer List levanta el token actual desde la cookie jar, deja de quedarse pegado a un valor viejo.

El resultado

Después de esos cambios, al correr la colección:

Request Resultado
GET - Login Page 200
POST - Login 200
POST - Customer List 200
Postman Collection Runner showing all three requests returning 200 OK after implementing dynamic CSRF token
El resultado: las tres requests pasan con 200. El flujo completo es estable y reutilizable con tokens dinámicos.

Recién ahí el flujo se vuelve real y reutilizable. No es solo "mandar requests". Es manejar cookies, autenticación, tokens dinámicos y dependencia entre requests.

La lección central

La lección más importante de este paso no es técnica. Es conceptual.

Es tentador pensar: "si la request se parece mucho a la del navegador, debería andar". A veces alcanza. A veces no.

En una app con seguridad real, ciertos valores no se "copian". Se resuelven en tiempo de ejecución.

El CSRF token no es un dato más. Es parte del contrato de seguridad de la aplicación. Por eso no sirve pegarlo una vez, no sirve confiar en un valor viejo, y no sirve asumir que la request va a seguir funcionando horas después.

Hay que leerlo dinámicamente.

Las capas de estado en Postman

Postman maneja varias capas de estado simultáneamente, y al principio eso puede marear:

  • Una request puede tener headers visibles en su pestaña Headers
  • El botón Cookies muestra lo que hay en la cookie jar
  • Algunas cookies se agregan automáticamente cuando una response trae Set-Cookie
  • Una variable como csrfToken puede existir sin crearla manualmente, porque un script la setea al vuelo
  • Una request puede funcionar sola pero fallar dentro de un flujo, o al revés

Cuando se entiende que Postman está manejando tres cosas al mismo tiempo — el request visible, las cookies persistidas por dominio, y las variables que scripts van actualizando — deja de parecer aleatorio.

Por qué importar desde DevTools sirve, pero no alcanza

Importar con Copy as cURL (bash) es útil porque trae a Postman una versión muy parecida a la request real del navegador. Sirve para no inventar la request, partir de algo fiel a la app, y ver método, URL, headers, body y cookies iniciales.

Chrome DevTools Network tab showing right-click context menu with Copy as cURL bash option highlighted
El punto de partida: Copy as cURL (bash) desde DevTools permite importar requests reales a Postman como base inicial.

Pero no alcanza por sí solo. Porque una vez importado, todavía hay que separar: qué headers son realmente necesarios, qué valores son dinámicos, qué cosas puede manejar Postman por su cuenta, y qué parte conviene automatizar con scripts.

Copy as cURL trae la foto del momento. Los scripts y variables dan comportamiento reutilizable.

Resumen

Ahora la colección puede: abrir la página de login, capturar contexto de seguridad, loguearse, y pedir la grilla de clientes — todo con tokens dinámicos.

La diferencia parece pequeña, pero no lo es.

Antes eran tres requests copiadas.
Ahora es un flujo que entiende estado.

Pero lo más valioso no es la colección en sí. Es entender por qué antes fallaba. Una request que "anda" sin saber por qué sirve poco. Una request que falló, se desarmó, se entendió y se hizo estable — eso es API testing real.

Código final

POST - Login → Pre-request

const csrfCookie = pm.cookies.get('CSRF-TOKEN');

if (csrfCookie) {
    pm.variables.set('csrfToken', csrfCookie);
    console.log('csrfToken actualizado desde cookie jar');
} else {
    console.log('No se encontró la cookie CSRF-TOKEN');
}

POST - Login → Header

x-csrf-token: {{csrfToken}}

POST - Customer List → Pre-request

const csrfCookie = pm.cookies.get('CSRF-TOKEN');

if (csrfCookie) {
    pm.variables.set('csrfToken', csrfCookie);
    console.log('csrfToken actualizado en Customer List desde cookie jar');
} else {
    console.log('No se encontró CSRF-TOKEN en Customer List');
}

POST - Customer List → Header

x-csrf-token: {{csrfToken}}

Qué sigue

El siguiente paso es convertir estas requests en verificaciones reales — no solo requests que responden 200, sino tests que validen estructura, datos y comportamiento del backend.


Esta serie se construye desde una app real (https://demo.serenity.is/Account/Login/?ReturnUrl=%2F), no desde una API pública de práctica aislada: leyendo requests en DevTools, llevándolas a Postman y entendiendo por qué fallan antes de automatizarlas.