Cómo configurar Nginx con CORS con múltiples orígenes

Configurar Nginx para que sea compatible con Cross origin resource sharing, incluso con varios orígenes, es sencillo si tienes el truco. En este artículo configuraremos un servidor Nginx para que CORS no vuelva a molestarte. Incluye el código.
Cómo configurar Nginx con CORS con múltiples orígenes

Hace un par de meses publiqué un artículo hablando sobre el Cross-Origin Resource Sharing (más conocido por los amigos como CORS) en el que explicaba qué es CORS, cómo te afecta y cómo puedes solventar los problemas de conexión a los que todo desarrollar debe enfrentarse por primera vez en algún momento 😱

Las explicaciones fueron teóricas para que entendieras qué era y cómo funcionaba este protocolo. Y en dicho artículo explicamos una de las cabeceras más importantes de CORS: la cabecera Access-Control-Allow-Origin. Por repasar rápidamente, esta cabecera indicaba desde qué sitios web se permitía acceder a la API: si el origen de la llamada que el navegador iniciaba no coincidía con el valor de esta cabecera, la conexión era denegada. También te mencioné que en esta cabecera solo podía figurar, o bien un origen (si tienes varios orígenes hay que hacer un apaño), o bien poner un asterisco * para indicar que desde cualquier origen, algo que resulta tremendamente inseguro y que no es nada recomendable.

Te recuerdo que la cabecera origen (origin, en inglés) no solo es el dominio en el que estás navegando. El origen es la suma de protocolo, dominio (o subdominio) y puerto. De esta forma http://juannicolas.eu no es el mismo origen que https://www.juannicolas.eu, ya que cambia el protocolo y el subdominio www.

Desde un punto de vista teórico, comprendido. Pero, ¿y desde un punto de vista práctico? ¿Cómo puedo configurar un servidor Nginx para que respete CORS? ¿Puedo habilitar más de un origen en la cabecera Access-Control-Allow-Origin ? 🤔

En este artículo voy a explicarte cómo configurar un servidor web Nginx para que funcione perfectamente en CORS e incluso te permita incluir varios orígenes en la cabecera Access-Control-Allow-Origin desde un punto de vista funcional. Así que vete instalando Nginx en tu máquina (o en un contenedor de Docker para hacer las pruebas) y comenzamos.

Entendiendo el fichero de configuración de Nginx

Pero, antes que nada, para los desarrolladores más inexpertos, vamos a repasar qué es Nginx y vamos a ver por encima cómo funciona el fichero de configuración de un sitio web en Nginx

Nginx es un servidor web de código abierto que se encarga de atender peticiones realizadas por los clientes (navegadores), respondiendo con la información oportuna y que, también, permite su uso como proxy inverso, caché y balanceador de carga. Este software, pronunciado como ényin-ex, nace para solventar el problema de cómo gestionar 10.000 peticiones concurrentes.

En otras palabras, cuando un cliente (tu navegador o tu App) hace una petición a tu dominio, Nginx recibe la petición y la analiza, devolviendo un fichero estático (un fichero javascript, un estilo css o un fichero html) o bien, se la pasa al software que corresponda (un motor de PHP, una instancia de Node.js, etc.). Nginx es tremendamente bueno para esta misión, pues es rápido y consume muy pocos recursos comparado con otros servidores web. Y es por ello que también se usa como caché HTML (guardar el código HTML que otro software ha generado y evitar gastar recursos para generar el mismo resultado) o como balanceador de carga (distribuir las peticiones hacia otros sistemas de forma controlada).

Nginx, al igual que el resto de servidores web, requiere un fichero de configuración para que sepa qué dominios tiene que atender y en qué puertos. El fichero de configuración clásico de un dominio en Nginx tiene una forma parecida a esta:

server {
    # CONFIGURACIÓN A NIVEL DE SERVER
    listen       80;
    server_name  api.midominio.com;
    access_log   logs/api.midominio.com.access.log  main;

    location ~ ^/(images|javascript|js|css|flash|media|static)/  {
      # CONFIGURACIÓN A NIVEL DE LOCATION
      root    /var/www/virtual/api.midominio.com/public;
      expires 30d;
    }

    location / {
      # CONFIGURACIÓN A NIVEL DE LOCATION
      proxy_pass      http://127.0.0.1:8080;
    }
}

Como puedes ver, y por lo general, cuando creamos o habilitamos un sitio web, creamos un fichero que contenga esta información de base. Es cierto que cuanto más grande o complejo es el proyecto, más rutas o configuraciones tendrá, pero este está bien para empezar.

En este fichero de configuración definimos cómo se llama el dominio al que Nginx tiene que atender, qué tipo de conexión (o dónde están alojados los certificados en el caso HTTPS) y algo más de información con respecto al servidor (común a todas las rutas que en él se encuentren). Posteriormente, definimos las rutas (o más bien, conjunto de rutas) en uno o varios bloques precedidos por la palabra clave location. Es decir, si el cliente solicita un estático, búscalo en esta carpeta; si solicita el directorio /admin, búscalo por allá; etc.

Bien, pues ahora que conocemos el esqueleto de un fichero de configuración de Nginx, ya podemos pasar al siguiente nivel: hacer que Nginx adjunte las cabeceras de Cross-Origin Resource Sharing.

Incluyendo CORS en Nginx con un solo origen

Recapitulando, ya tenemos Nginx atendiendo en un dominio y preparado para devolver nuestros ficheros web. El problema al que nos enfrentamos ahora es que este Nginx tiene que adjuntar las cabeceras de CORS para que las peticiones de nuestro navegador funcionen correctamente. Estas cabeceras son las que vimos en el artículo al que te hacía referencia al comienzo: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, y Access-Control-Expose-Headers. Por repasar, estas cabeceras indican desde qué dominio es válido hacer una petición, qué métodos son compatibles, qué cabeceras adicionales puede llevar la petición y qué cabeceras de la respuesta deberían estar disponibles para la APP, respectivamente.

Suponte que Nginx está controlando el dominio miapi.es, mientras que nuestra aplicación web está alojada en el dominio miapp.es. Esto es una simple ayuda visual, realmente valdría cualquier subdominio o dominio, pero para que el fichero de configuración sea más sencillo de leer, usaré estos dominios sencillos. También recuerda que según el protocolo de CORS, en determinadas ocasiones y antes de enviar la información al servidor, el navegador hace una primera petición al servidor para cerciorarse de que el servidor comprende el protocolo CORS. A esta petición se la llama preflight y se emplea el método HTTP llamado options. Explicado esto, añadimos lo siguiente a nuestra configuración de Nginx:

server {
    # CONFIGURACIÓN A NIVEL DE SERVER
    listen       80;
    server_name  api.midominio.com;

    location / {
      #Adjuntamos las cabeceras de CORS
      add_header 'Access-Control-Allow-Origin' 'https://miapp.es' always;
      add_header 'Access-Control-Allow-Credentials' 'true' always;
      add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
      add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
 
      #Comprobamos si es una petición preflight y la "cacheamos" 20 días
      if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
      }

       # RESTO de configuración. Omitido por legibilidad
    }
}

¡Listo! Nuestro Nginx ya adjunta las cabeceras de Cross-Origin Resource Sharing. Autorizamos al origen https://miapp.es a que puede enviar peticiones GET, PUT, POST, DELETE y OPTIONS y con las cabeceras Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With.

Teóricamente, ya hemos conseguido lo que queríamos, adjuntar las cabeceras CORS a las peticiones con Nginx. Sin embargo, necesitamos meter todas estas instrucciones en cada bloque location y, la verdad, es que queda muy feo y se lee muy mal. Como somos buenos desarrolladores y nos gusta tener todas las configuraciones más limpias y más legibles, vamos a sacar las configuraciones de CORS a un snippet (fragmentos reutilizables de códigos para Nginx). De esta forma, podremos usarlos en otras configuraciones y dejaremos el código más legible.

# /etc/nginx/snippets/cors_configs.conf
#Adjuntamos las cabeceras de CORS
add_header 'Access-Control-Allow-Origin' 'https://miapp.es' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
 
#Comprobamos si es una petición preflight y la "cacheamos" 20 días
if ($request_method = 'OPTIONS') {
  add_header 'Access-Control-Max-Age' 1728000;
  add_header 'Content-Type' 'text/plain charset=UTF-8';
  add_header 'Content-Length' 0;
  return 204;
}

# /etc/nginx/sites-available/miapi.es.conf
server {
    # CONFIGURACIÓN A NIVEL DE SERVER
    listen       80;
    server_name  api.midominio.com;

    location / {
      include 'snippets/cors_configs.conf'
       # RESTO de configuración. Omitido por legibilidad
    }
}

Incluyendo CORS en Nginx con múltiples orígenes

Pero, ¿y si tenemos dos o más orígenes legítimos que requieren consultar una API con CORS? Es decir, la API de miapi.es se tiene que consultar desde miapp.es pero también desde miapp.es:4000 y adminapp.es. Pues la configuración anterior de CORS no nos sería válida porque solo sería legítimo el acceso desde miapp.es; ningún otro origen estaría admitido. Pero tranquil@, todo tiene solución.

Nos enfrentamos ahora al problema de que varios orígenes tienen que ser legítimos, pero el protocolo Cross-Origin Resource Sharing solo permite un origen en su cabecera Access-Control-Allow-Origin. Sin embargo, si esta cabecera fuese dinámica, podríamos responder de forma diferente dependiendo del origen de la petición que llegue a Nginx. Es decir, analizar la cabecera origin que llega a Nginx, comprobar si es legítima y responder de forma dinámica en relación con la comprobación. La buena noticia es que esto sí es posible en Nginx y, de hecho, vamos a hacerlo a continuación. Esto nos permitirá tener varios orígenes válidos.

En primer lugar, déjame presentarte el código que validará si el origen es legítimo o no. En el siguiente fragmento de código Nginx, vamos a definir los orígenes válidos y permitidos. Te recuerdo, siguiendo nuestro ejemplo, que deberíamos permitir los dominios miapp.es, miapp.es:4000 y adminapp.es. En tu caso, deberás poner los dominios y subdominios que quieras permitir:

# /etc/nginx/snippets/cors-sites.conf
map "$http_origin" $cors {
  default '';
  "~^https?://miapp.es(:[0-9]+)?$" "$http_origin";
  "~^https?://adminapp?$" "$http_origin";
}

Este fragmento es muy simple aunque a primera vista puede parecer curioso. Te cuento: el map en Nginx es algo así como un switch case de un lenguaje de programación. Ten en cuenta que la cabecera origin del navegador se traduce en la variable $http_origin; esto lo hace Nginx por nosotros.

Traducido en texto, lo que estamos diciendo es: "basándonos en lo que valga $http_origin, establece la variable $cors".

  • Si la variable http_origin se ajusta a la expresión regular ~^https?://miapp.es(:[0-9]+)?$, entonces cors = http_origin.
  • Si la variable http_origin se ajusta a la expresión regular ~^https?://adminapp?$, entonces $cors = $http_origin".
  • Si la variable http_origin no se ajusta a nada, $cors = "".

De esta forma, si la petición proviene de un origen autorizado, $cors valdrá dicho origen. En caso contrario, $cors valdrá "vacío". Básicamente, estamos conociendo el origen y validándolo de un plumazo.

Hecho esto, y habiendo entendido el código, tenemos que editar el snippet cors_configs.conf que creamos en la sección anterior para que imprima las cabeceras o no según el valor de la variable $cors; es decir, si $cors no es vacío, imprime las cabeceras CORS. El fichero queda como sigue:

# /etc/nginx/snippets/cors_configs.conf
#Determinamos si es un origen válido y lo establecemos en la variable $cors.
map "$http_origin" $cors {
  default '';
  "~^https?://miapp.es(:[0-9]+)?$" "$http_origin";
  "~^https?://adminapp?$" "$http_origin";
}

#Adjuntamos las cabeceras de CORS siempre que sea un origen válido ($cors no debe estar vacío)
if ($cors != "") {
  add_header 'Access-Control-Allow-Origin' "$cors" always; # <-- Variable $cors
  add_header 'Access-Control-Allow-Credentials' 'true' always;
  add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
  add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
}

#Comprobamos si es una petición preflight y la "cacheamos" 20 días
if ($request_method = 'OPTIONS') {
  add_header 'Access-Control-Max-Age' 1728000;
  add_header 'Content-Type' 'text/plain charset=UTF-8';
  add_header 'Content-Length' 0;
  return 204;
}

¡Listo! Ahora Nginx comprobará dinámicamente el origen dentro de una lista de orígenes permitidos. Si coincide, adjuntará las cabeceras de CORS para que el navegador no ponga ningún problema. Por el contrario, si el origen de la petición no es legítimo, no las imprimirá, así que el navegador bloqueará las peticiones.

Para usar este código, bastará con incluir el fragmento include 'snippets/cors_configs.conf' que vimos antes (pero siempre en el nivel de location). ¡Y ya está! Ya tenemos un Nginx que nos permite asegurar un CORS seguro y cómodo para todos nuestros dominios y subdominios autorizados.

Conclusión

Muchas veces, generalmente por desconocimiento, odiamos algo que, realmente, está haciendo nuestra navegación más segura. Sin duda, este es el caso del Intercambio de Recursos de Origen Cruzado. Muchos desarrolladores le tienen miedo o cierta aversión porque no saben realmente qué es y cómo vela por nosotros (y en este grupo yo me incluía).

Sin embargo, ahora que hemos aprendido cómo configurar un servidor Nginx para que funcione respetando el Intercambio de Recursos de Origen Cruzado, tanto en un solo origen como en múltiples, este protocolo no debería darte ningún miedo y deberías estar más tranquilo sabiendo que ningún otro origen que tú autorices puede hacerte peticiones desde un navegador.

Espero haberte hecho la vida un poco más fácil y haberte ayudado a que no pongas un wildcard en la cabecera Access-Control-Allow-Origin nunca, nunca, nunca más.

¡Qué tengas un feliz coding!