En este artículo, veremos cómo habilitar CORS (Cross-Origin Resource Sharing) con la cookie HTTPOnly para asegurar nuestros tokens de acceso.

Hoy en día, los servidores backend y los clientes frontend se despliegan en dominios diferentes. Por lo tanto, el servidor tiene que habilitar CORS para permitir a los clientes comunicarse con el servidor en los navegadores.

Además, los servidores están implementando la autenticación sin estado para una mejor escalabilidad. Los tokens se almacenan y mantienen en el lado del cliente, pero no en el lado del servidor como la sesión. Por seguridad, es mejor almacenar los tokens en cookies HTTPOnly.

¿Por qué se bloquean las solicitudes Cross-Origin?

Supongamos que nuestra aplicación frontend se despliega en https://app.geekflare.com/es. Un script cargado en https://app.geekflare.com/escansólo solicita recursos del mismo origen.

Siempre que intentemos enviar una solicitud de origen cruzado a otro dominio https://api.geekflare.com/es o a otro puerto https://app.geekflare.com/es:3000 o a otro esquema http://app.geekflare.com/es, la solicitud de origen cruzado será bloqueada por el navegador.

Pero por qué la misma petición bloqueada por el navegador puede ser enviada desde cualquier servidor backend usando curl request o enviada usando herramientas como el postman sin ningún problema CORS. En realidad es por seguridad para proteger a los usuarios de ataques como CSRF(Cross-Site Request Forgery).

Pongamos un ejemplo, supongamos que cualquier usuario iniciara sesión en su propia cuenta PayPal en su navegador. Si podemos enviar una solicitud cross-origin a paypal. com desde un script cargado en otro dominio malicious. com sin ningún error CORS/bloqueo como si enviáramos la solicitud same-origin.

Los atacantes pueden enviar fácilmente su página maliciosa https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account convirtiéndola en short-URL para ocultar la URL real. Cuando el usuario haga clic en el enlace malicioso, el script cargado en el dominio malicious. com enviará una solicitud de origen cruzado a PayPal para transferir el importe del usuario a la cuenta PayPal del atacante se ejecutará. Todos los usuarios que hayan iniciado sesión en su cuenta PayPal y hayan pulsado este enlace malicioso perderán su dinero. Cualquiera puede robar dinero fácilmente sin que el usuario de la cuenta PayPal lo sepa.

Por este motivo, los navegadores bloquean todas las solicitudes de origen cruzado.

¿Qué es CORS(Cross-Origin Resource Sharing)?

CORS es un mecanismo de seguridad basado en cabeceras que utiliza el servidor para indicar al navegador que envíe una solicitud de origen cruzado desde dominios de confianza.
El servidor habilitado con cabeceras CORS se utiliza para evitar que las peticiones cross-origin sean bloqueadas por los navegadores.

¿Cómo funciona CORS?

El servidor ya ha definido su dominio de confianza en su configuración CORS. Cuando enviamos una petición al servidor, la respuesta indicará al navegador el dominio solicitado es de confianza o no en su cabecera.

Existen dos tipos de peticiones CORS:

  • Solicitud simple
  • Solicitud previa

Solicitud simple:

CORS-simple request flow tells that it sends a cross-origin request but when it received response. It checks for headers.

  • El navegador envía la solicitud a un dominio de origen cruzado con origen(https://app.geekflare.com/es).
  • El servidor devuelve la respuesta correspondiente con métodos permitidos y origen permitido.
  • Tras recibir la solicitud, el navegador comprobará que el valor de la cabecera de origen enviada(https://app.geekflare.com/es) y el valor de access-control-allow-origin recibido(https://app.geekflare.com/es) son iguales o comodín(*). En caso contrario, arrojará un error CORS.

Solicitud de verificación previa:

CORS-Preflight Request Image which show the flow of cross-origin request with OPTIONS preflight request before sending actual request for verifying headers.

  • Dependiendo del parámetro de solicitud personalizado de la solicitud de origen cruzado, como métodos (PUT, DELETE) o cabeceras personalizadas o un tipo de contenido diferente, etc. El navegador decidirá enviar una solicitud OPTIONS previa para comprobar si la solicitud real es segura de enviar o no.
  • Tras recibir la respuesta (código de estado: 204, que significa sin contenido), el navegador comprobará los parámetros de control de acceso permitidos para la solicitud real. Si los parámetros de la petición están permitidos por el servidor. La solicitud cross-origin real enviada y recibida

Si access-control-allow-origin:*,entonces la respuesta está permitida para todos los orígenes. Pero no es seguro a menos que lo necesite.

¿Cómo habilitar CORS?

Para habilitar CORS para cualquier dominio, habilite las cabeceras CORS para permitir origen, métodos, cabeceras personalizadas, credenciales, etc.

El navegador lee la cabecera CORS del servidor y permite las peticiones reales del cliente sólo después de verificar los parámetros de la petición.

  • Access-Control-Allow-Origin: Para especificar dominios exactos(https://app.geekflate.com, https://lab.geekflare.com/es) o comodines(*)
  • Access-Control-Allow-Methods: Para permitir los métodos HTTP(GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) que sólo nosotros necesitamos.
  • Access-Control-Allow-Headers: Para permitir sólo Cabeceras específicas(Autorización, csrf-token)
  • Access-Control-Allow-Credentials: Valor booleano utilizado para permitir cross-origin-credentials(cookies, authorization header).
  • Access-Control-Max-Age : Indica al navegador que almacene en caché la respuesta de verificación previa durante cierto tiempo.
  • Access-Control-Expose-Headers: Especifica las cabeceras que son accesibles por el script del lado del cliente.

Para habilitar CORS en el servidor web apache y Nginx, siga este tutorial.

Habilitar CORS en ExpressJS

Tomemos una aplicación ExpressJS de ejemplo sin CORS:

const express = require('express');
const app = express()

app.get('/users', function (req, res, next) {
  res.json({msg: 'obtener usuario'})
});

app.post('/users', function (req, res, next) {
    res.json({msg: 'crear usuario'})
});

app.put('/users', function (req, res, next) {
    res.json({msg: 'Actualización de usuario'})
});

app.listen(80, function () {
  console.log('Servidor web habilitado para CORS escuchando en el puerto 80')
})

En el ejemplo anterior, hemos habilitado el punto final de la API de usuarios para los métodos POST, PUT, GET pero no para el método DELETE.

Para habilitar CORS fácilmente en la aplicación ExpressJS, puede instalar el cors

npm install cors

Access-Control-Allow-Origin

Habilitar CORS para todo el dominio

app.use(cors({
    origen: '*'
}));

Habilitar CORS para un único dominio

app.use(cors({
    origen: 'https://app.geekflare.com/es'
}));

Si desea permitir CORS para el origen https://app.geekflare.com/es y https://lab.geekflare.com/es

app.use(cors({
    origen: [
        'https://app.geekflare.com/es',
        'https://lab.geekflare.com/es'
    ]
}));

Access-Control-Allow-Methods

Para habilitar CORS para todos los métodos, omita esta opción en el módulo CORS en el ExpressJS. Pero para habilitar métodos específicos(GET, POST, PUT).

app.use(cors({
    origen: [
        'https://app.geekflare.com/es',
        'https://lab.geekflare.com/es'
    ],
    métodos: ['GET', 'PUT', 'POST']
}));

Control-de-acceso-Allow-Headers

Se utiliza para permitir que se envíen cabeceras distintas de las predeterminadas con las solicitudes reales.

app.use(cors({
    origen: [
        'https://app.geekflare.com/es',
        'https://lab.geekflare.com/es'
    ],
    métodos: ['GET', 'PUT', 'POST'],
    encabezados permitidos: ['Content-Type', 'Authorization', 'x-csrf-token']
}));

Access-Control-Allow-Credentials

Omita esto si no quiere decirle al navegador que permita las credenciales en la solicitud incluso cuando withCredentials esté establecido en true.

app.use(cors({
    origen: [
        'https://app.geekflare.com/es',
        'https://lab.geekflare.com/es'
    ],
    métodos: ['GET', 'PUT', 'POST'],
    encabezados permitidos: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credenciales: true
}));

Access-Control-Max-Age

Indicar al navegador que almacene en caché la información de respuesta previa a la comprobación durante un segundo especificado. Omita esto si no desea almacenar en caché la respuesta.

app.use(cors({
    origen: [
        'https://app.geekflare.com/es',
        'https://lab.geekflare.com/es'
    ],
    métodos: ['GET', 'PUT', 'POST'],
    encabezados permitidos: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credenciales: true,
    maxAge: 600 
}));

La respuesta de verificación previa almacenada en caché estará disponible durante 10 minutos en el navegador.

Access-Control-Expose-Headers

app.use(cors({
    origen: [
        'https://app.geekflare.com/es',
        'https://lab.geekflare.com/es'
    ],
    métodos: ['GET', 'PUT', 'POST'],
    encabezados permitidos: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credenciales: true,
    maxAge: 600,
    exposedHeaders: ['Content-Range', 'X-Content-Range']
}));

Si ponemos el comodín(*) en exposedHeaders, no expondrá la cabecera Authorization. Así que tenemos que exponer explícitamente como a continuación

app.use(cors({
    origen: [
        'https://app.geekflare.com/es',
        'https://lab.geekflare.com/es'
    ],
    métodos: ['GET', 'PUT', 'POST'],
    encabezados permitidos: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credenciales: true,
    maxAge: 600,
    exposedHeaders: ['*', 'Authorization', ]
}));

Lo anterior expondrá todas las cabeceras y también la cabecera Authorization.

¿Qué es una cookie HTTP?

Una cookie es un pequeño fragmento de datos que el servidor enviará al navegador del cliente. En posteriores peticiones, el navegador enviará todas las cookies relacionadas con el mismo dominio en cada petición.

Una cookie tiene su atributo, que puede ser definido para hacer que una cookie funcione de forma diferente según nuestras necesidades.

  • Nombre Nombre de la cookie.
  • valor: datos de la cookie correspondientes al nombre de la cookie
  • Dominio: las cookies se enviarán sólo al dominio definido
  • Ruta : las cookies se enviarán sólo a la ruta con prefijo URL definida. Supongamos que hemos definido la ruta de nuestra cookie como path=’admin/’. Las cookies no se enviarán para la URL https://geekflare.com/es/expire/ sino que se enviarán con el prefijo de URL https://geekflare.com/es/admin/
  • Max-Age/Expires(número en segundos): Cuándo debe caducar la cookie. El tiempo de vida de la cookie hace que la cookie no sea válida después del tiempo especificado.
  • HTTPOnly(booleano): El servidor backend puede acceder a esa cookie HTTPOnly pero no el script del lado del cliente cuando es true.
  • Secure(Booleano): Las cookies sólo se envían a través de un dominio SSL/TLS cuando es verdadero.
  • sameSite(cadena [Strict, Lax, None]): Se utiliza para habilitar/restringir las cookies enviadas a través de peticiones cross-site. Para conocer más detalles sobre las cookies sameSite consulte MDN. Acepta tres opciones Strict, Lax, None. Valor seguro de la cookie establecido en true para la configuración de la cookie sameSite=None.

¿Por qué cookie HTTPOnly para tokens?

Almacenar el token de acceso enviado desde el servidor en el almacenamiento del lado del cliente como almacenamiento local, DB indexada y cookie (HTTPOnly no configurado como true) son más vulnerables a un ataque XSS. Suponga que alguna de sus páginas es débil a un ataque XSS. Los atacantes pueden hacer un uso indebido de los tokens de usuario almacenados en el navegador.

Las cookies HTTPOnly sólo son establecidas/obtenidas por el servidor/backend pero no en el lado del cliente.

El script del lado del cliente tiene restringido el acceso a esa cookie HTTPonly. Así que las cookies HTTPOnly no son vulnerables a los ataques XSS y son más seguras. Porque sólo es accesible por el servidor.

Habilitar cookie HTTPOnly en backend habilitado para CORS

Habilitar la cookie en CORS necesita la siguiente configuración en la aplicación/servidor.

  • Establezca el encabezado Access-Control-Allow-Credentials en true.
  • Access-Control-Allow-Origin y Access-Control-Allow-Headers no deben ser comodines(*).
  • El atributo cookie sameSite debe ser None.
  • Para habilitar el valor sameSite a none, establezca el valor secure a true: Habilitar backend con certificado SSL/TLS para trabajar en el nombre de dominio.

Veamos un código de ejemplo que establece un token de acceso en la cookie HTTPOnly tras comprobar las credenciales de inicio de sesión.

const express = require('express'); 
const app = express();
const cors = require('cors');

app.use(cors({ 
  origen: [ 
    'https://app.geekflare.com/es', 
    'https://lab.geekflare.com/es' 
  ], 
  métodos: ['GET', 'PUT', 'POST'], 
  encabezados permitidos: ['Content-Type', 'Authorization', 'x-csrf-token'], 
  credenciales: true, 
  maxAge: 600, 
  exposedHeaders: ['*', 'Authorization' ] 
}));

app.post('/login', function (req, res, next) { 
  res.cookie('access_token', access_token, {
    expires: new Date(Date.now() (3600 * 1000 * 24 * 180 * 1)), //segundo min hora días año
    secure: true, // establecer en true si su uso de https o samesite es ninguno
    httpOnly: true, // sólo backend
    sameSite: 'none' // establecer a ninguno para cross-request
  });

  res.json({ msg: 'Inicio de sesión con éxito', access_token });
});

app.listen(80, function () { 
  console.log('Servidor web habilitado para CORS escuchando en el puerto 80') 
}); 

Puede configurar CORS y las cookies HTTPOnly implementando los cuatro pasos anteriores en su lenguaje backend y servidor web.

Puede seguir este tutorial para apache y Nginx para habilitar CORS siguiendo los pasos anteriores.

withCredentials para solicitud Cross-Origin

Credenciales(Cookie, Autorización) enviadas con la petición del mismo-origen por defecto. Para cross-origin, tenemos que especificar el withCredentials a true.

API XMLHttpRequest

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://api.geekflare.com/es/user', true);
xhr.withCredentials = true;
xhr.send(null);

Obtener API

fetch('http://api.geekflare.com/es/user', {
  credenciales: 'incluir'
});

JQuery Ajax

$.ajax({
   url: 'http://api.geekflare.com/es/user',
   xhrFields: {
      withCredentials: true
   }
});

Axios

axios.defaults.withCredentials = true

Conclusión

Espero que el artículo anterior le ayude a entender cómo funciona CORS y habilitar CORS para peticiones de origen cruzado en el servidor. Por qué almacenar cookies en HTTPOnly es seguro y cómo se utiliza withCredentials en los clientes para peticiones de origen cruzado.