¿Cómo habilitar CORS con HTTPOnly Cookie para proteger el token?

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

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

Además, los servidores están implementando 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 en la sesión. Por seguridad, es mejor almacenar tokens en cookies HTTPOnly.

Tabla de contenido

¿Por qué se bloquean las solicitudes de origen cruzado?

Supongamos que nuestra aplicación frontend se implementó en https://app.kirukiru.es.com. Un script cargado en https://app.kirukiru.es.com solo puede solicitar recursos del mismo origen.

Cada vez que intentamos enviar una solicitud de origen cruzado a otro dominio https://api.kirukiru.es.com u otro puerto https://app.kirukiru.es.com:3000 u otro esquema http://app.kirukiru.es.com, el la solicitud de origen cruzado será bloqueada por el navegador.

Pero, ¿por qué la misma solicitud bloqueada por el navegador se envía desde cualquier servidor back-end usando curl request o se envía usando herramientas como el cartero sin ningún problema de CORS? En realidad, es por seguridad para proteger a los usuarios de ataques como CSRF (falsificación de solicitud entre sitios).

Tomemos un ejemplo, supongamos que cualquier usuario inició sesión en su propia cuenta de PayPal en su navegador. Si podemos enviar una solicitud de origen cruzado a paypal.com desde un script cargado en otro dominio malicioso.com sin ningún error/bloqueo CORS como si enviáramos la solicitud del mismo origen.

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 una URL corta para ocultar la URL real. Cuando el usuario hace clic en un enlace malicioso, el script cargado en el dominio malicioso.com enviará una solicitud de origen cruzado a PayPal para transferir el monto del usuario a la cuenta de PayPal del atacante que se ejecutará. Todos los usuarios que hayan iniciado sesión en su cuenta de PayPal y hayan hecho clic en este enlace malicioso perderán su dinero. Cualquiera puede robar dinero fácilmente sin el conocimiento del usuario de una cuenta de PayPal.

Por la razón anterior, los navegadores bloquean todas las solicitudes de origen cruzado.

¿Qué es CORS (intercambio de recursos de origen cruzado)?

CORS es un mecanismo de seguridad basado en encabezados que utiliza el servidor para indicarle al navegador que envíe una solicitud de origen cruzado desde dominios de confianza.
El servidor habilitado con encabezados CORS utilizados para evitar solicitudes de origen cruzado bloqueadas por navegadores.

¿Cómo funciona CORS?

Como el servidor ya definió su dominio de confianza en su configuración CORS. Cuando enviamos una solicitud al servidor, la respuesta le indicará al navegador que el dominio solicitado es confiable o no en su encabezado.

Existen dos tipos de solicitudes CORS:

  • solicitud sencilla
  • Solicitud de verificación previa

Solicitud sencilla:

  • El navegador envía la solicitud a un dominio de origen cruzado con origen (https://app.kirukiru.es.com).
  • El servidor devuelve la respuesta correspondiente con métodos permitidos y origen permitido.
  • Después de recibir la solicitud, el navegador verificará que el valor del encabezado de origen enviado (https://app.kirukiru.es.com) y el valor de origen de control de acceso recibido (https://app.kirukiru.es.com) son iguales o comodín

. De lo contrario, arrojará un error CORS.

  • Solicitud de verificación previa:
  • Dependiendo del parámetro de solicitud personalizado de la solicitud de origen cruzado, como métodos (PUT, DELETE) o encabezados personalizados o diferentes tipos de contenido, etc. El navegador decidirá enviar una solicitud de OPCIONES de verificación previa para verificar si la solicitud real es segura para enviar O no.

Después de recibir la respuesta (código de estado: 204, lo que significa que no hay contenido), el navegador verificará los parámetros de permiso de control de acceso para la solicitud real. Si los parámetros de solicitud están permitidos por el servidor. La solicitud real de origen cruzado 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 los encabezados de CORS para permitir el origen, los métodos, los encabezados personalizados, las credenciales, etc.

  • El navegador lee el encabezado CORS del servidor y permite solicitudes reales del cliente solo después de verificar los parámetros de la solicitud.
  • Access-Control-Allow-Origin: para especificar dominios exactos (https://app.geekflate.com, https://lab.kirukiru.es.com) o comodines
  • Access-Control-Allow-Methods: Para permitir los métodos HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) que solo nosotros necesitamos.
  • Access-Control-Allow-Headers: para permitir solo encabezados específicos (autorización, csrf-token)
  • Access-Control-Allow-Credentials: valor booleano utilizado para permitir credenciales de origen cruzado (cookies, encabezado de autorización).

Access-Control-Max-Age: le dice al navegador que almacene en caché la respuesta de verificación previa durante algún tiempo.

Access-Control-Expose-Headers: especifique los encabezados a los que se puede acceder mediante el script del lado del cliente.

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

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

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

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

app.put('/users', function (req, res, next) {
    res.json({msg: 'User update'})
});

app.listen(80, function () {
  console.log('CORS-enabled web server listening on port 80')
})

Habilitación de CORS en ExpressJS

Tomemos un ejemplo de aplicación ExpressJS sin CORS:

npm install cors

En el ejemplo anterior, hemos habilitado el extremo de la API de los 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

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

Acceso-Control-Permitir-Origen

app.use(cors({
    origin: 'https://app.kirukiru.es.com'
}));

Habilitación de CORS para todos los dominios

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

Habilitación de CORS para un solo dominio

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

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST']
}));

Acceso-Control-Permitir-Métodos

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

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token']
}));

Acceso-Control-Permitir-Encabezados

Se utiliza para permitir que los encabezados que no sean los predeterminados se envíen con solicitudes reales.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true
}));

Acceso-Control-Permitir-Credenciales

Omita esto si no desea decirle al navegador que permita las credenciales a pedido, incluso si withCredentials está establecido en verdadero.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600 
}));

Access-Control-Max-Age

Para intimar al navegador a almacenar en caché la información de respuesta de verificación previa en el caché durante un segundo específico. Omita esto si no desea almacenar en caché la respuesta.

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600,
    exposedHeaders: ['Content-Range', 'X-Content-Range']
}));

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

app.use(cors({
    origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
    ],
    methods: ['GET', 'PUT', 'POST'],
    allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
    credentials: true,
    maxAge: 600,
    exposedHeaders: ['*', 'Authorization', ]
}));

Acceso-Control-Exponer-Encabezados

Si ponemos el comodín

en los encabezados expuestos, no expondrá el encabezado de autorización. Así que tenemos que exponer explícitamente como a continuación

Lo anterior expondrá todos los encabezados y el encabezado de Autorización también.

  • ¿Qué es una cookie HTTP?
  • Una cookie es una pequeña porción de datos que el servidor enviará al navegador del cliente. En solicitudes posteriores, el navegador enviará todas las cookies relacionadas con el mismo dominio en cada solicitud.
  • La cookie tiene su atributo, que se puede definir para hacer que una cookie funcione de manera diferente según lo necesitemos.
  • Nombre Nombre de la cookie.
  • valor: datos de la cookie correspondientes al nombre de la cookie
  • Dominio: las cookies se enviarán solo al dominio definido
  • Ruta: cookies enviadas solo después de la ruta del prefijo URL definido. Supongamos que hemos definido nuestra ruta de cookies como path=’admin/’. Las cookies no se envían para la URL https://kirukiru.es.com/expire/ pero se envían con el prefijo de URL https://kirukiru.es.com/admin/
  • Max-Age/Expires (número en segundos): cuándo debe expirar la cookie. Una vida útil de la cookie hace que la cookie no sea válida después del tiempo especificado. [Strict, Lax, None]HTTPOnly(Boolean): el servidor backend puede acceder a esa cookie HTTPOnly pero no al script del lado del cliente cuando es verdadero. Seguro (booleano): las cookies solo se envían a través de un dominio SSL/TLS cuando son verdaderas.mismoSitio(cadena

): Se utiliza para habilitar/restringir las cookies enviadas en solicitudes entre sitios. Para conocer más detalles sobre las cookies sameSite ver

MDN

. Acepta tres opciones Estricto, Laxo, Ninguno. Valor seguro de cookies establecido en verdadero para la configuración de cookies sameSite=None.

¿Por qué la cookie HTTPOnly para tokens?

Almacenar el token de acceso enviado desde el servidor en el almacenamiento del lado del cliente, como el almacenamiento local, la base de datos indexada y las cookies (HTTPOnly no configurado como verdadero) es más vulnerable al ataque XSS. Suponga que alguna de sus páginas es débil a un ataque XSS. Los atacantes pueden hacer un mal uso de los tokens de usuario almacenados en el navegador.

Las cookies HTTPOnly solo las establece/obtiene el servidor/backend, pero no del lado del cliente.

  • Script del lado del cliente restringido para acceder a esa cookie HTTPonly. Por lo tanto, las cookies HTTPOnly no son vulnerables a los ataques XSS y son más seguras. Porque solo es accesible por el servidor.
  • Habilite la cookie HTTPOnly en el 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 verdadero.

Access-Control-Allow-Origin y Access-Control-Allow-Headers no deben ser comodines

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

app.use(cors({ 
  origin: [ 
    'https://app.geekflare.com', 
    'https://lab.geekflare.com' 
  ], 
  methods: ['GET', 'PUT', 'POST'], 
  allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'], 
  credentials: 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)), //second min hour days year
    secure: true, // set to true if your using https or samesite is none
    httpOnly: true, // backend only
    sameSite: 'none' // set to none for cross-request
  });

  res.json({ msg: 'Login Successfully', access_token });
});

app.listen(80, function () { 
  console.log('CORS-enabled web server listening on port 80') 
}); 

.

El atributo mismoSitio de la cookie debe ser Ninguno.

Para habilitar el valor de sameSite en ninguno, establezca el valor seguro en verdadero: habilite el backend con certificado SSL/TLS para que funcione en el nombre de dominio.

Veamos un código de ejemplo que establece un token de acceso en la cookie HTTPOnly después de verificar las credenciales de inicio de sesión.

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

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

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

fetch('http://api.kirukiru.es.com/user', {
  credentials: 'include'
});

withCredentials for Cross-Origin request

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

Credenciales (Cookie, Autorización) enviadas con la solicitud del mismo origen de forma predeterminada. Para el origen cruzado, tenemos que especificar withCredentials en verdadero.

axios.defaults.withCredentials = true

API XMLHttpRequest

Obtener API

JQueryAjaxAxiosConclusión Espero que el artículo anterior lo ayude a comprender cómo funciona CORS y habilite CORS para solicitudes de origen cruzado en el servidor. Por qué es seguro almacenar cookies en HTTPOnly y cómo se usan withCredentials en clientes para solicitudes de origen cruzado.