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 que los clientes se comuniquen 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 en 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.
Un script cargado en https://app.geekflare.comcan
sólo solicita recursos del mismo origen
Siempre que intentemos enviar una solicitud de origen cruzado a otro dominio https://api.geekflare.com
o a otro puerto https://app.geekflare.com:3000
o a otro esquema http://app.geekflare.com,
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 inicia 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 esta razón, los navegadores bloquean todas las peticiones 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 de origen cruzado 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
- Petición simple
- Solicitud previa
Solicitud simple:
- El navegador envía la solicitud a un dominio de origen cruzado con origen(https://app.geekflare.com).
- El servidor devuelve la respuesta correspondiente con los métodos permitidos y el origen permitido.
- Tras recibir la solicitud, el navegador comprobará que el valor de la cabecera de origen enviada(https://app.geekflare.com) y el valor de access-control-allow-origin recibido(https://app.geekflare.com) son iguales o comodín(*). En caso 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 cabeceras personalizadas o diferente tipo de contenido, etc. El navegador decidirá enviar una solicitud OPTIONS de verificación 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) 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)
- Control de acceso - Permitir credenciales: 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: '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('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 cor
s
Access-Control-Allow-Origin
Habilitación de CORS para todo el dominio
app.use(cors({
origin: '*'
}
))
Habilitación de CORS para un único dominio
app.use(cors({
origen: 'https://app.geekflare.com'
}))
Si desea permitir CORS para el origen https://app.geekflare.com y https://lab.geekflare.com
aplicación
.use(cors({
origen: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
]
}))
Control-de-acceso-Permitir-métodos
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',
'https://lab.geekflare.com'
],
métodos: ['GET', 'PUT', 'POST']
}
))
Access-Control-Allow-Headers
Se utiliza para permitir que se envíen cabeceras distintas de las predeterminadas con las 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']
})
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({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: 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({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: 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({
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']
}
))
Si ponemos el comodín(*) en exposedHeaders , no expondrá la cabecera Autorización. Así que tenemos que exponer explícitamente como a continuación
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', ]
}
))
Lo anterior expondrá todas las cabeceras y también la de Autorización
¿Qué es una cookie HTTP?
Una cookie es un pequeño fragmento de datos que el servidor enviará al navegador del cliente. En peticiones posteriores, el navegador enviará todas las cookies relacionadas con el mismo dominio en cada petición.
La cookie tiene sus atributos, que pueden definirse 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/expire/ sino que se enviarán con el prefijo de URL https://geekflare.com/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.
- Seguro(Booleano): Las cookies sólo se envían a través de un dominio SSL/TLS cuando es verdadero.
- sameSite(cadena [Estricta, Laxa, Ninguna]): 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 galleta (HTTPOnly no configurado a true) son más vulnerables a ataques XSS. Suponga que alguna de sus páginas es débil a un ataque XSS. Los atacantes podrían utilizar indebidamente 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 CORS habilitado backend
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({
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)), /segundo min hora días año
secure: true, // establecer en true si está utilizando https o samesite es none
httpOnly: true, // sólo backend
sameSite: 'none' // establecer a none para cross-request
});
res.json({ msg: 'Login Successfully', access_token });
});
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
}
)
Puede configurar CORS y 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 peticiones 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/user', true);
xhr
.withCredentials = true;
xhr
.send(null)
Obtener API
fetch('http://api.geekflare.com/user', {
credentials: 'include'
}
)
JQuery Ajax
$.ajax({
url: 'http://api.geekflare.com/user',
xhrFields: {
withCredentials: true
}
}
)
Axios
axios
.defaults.withCredentials =
verdadero
Conclusión
Espero que el artículo anterior le ayude a entender cómo funciona CORS y a 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 clientes para peticiones de origen cruzado.