Establecer cookies para solicitudes de origen cruzado

97

¿Cómo compartir cookies de origen cruzado? Más específicamente, ¿cómo usar el Set-Cookieencabezado en combinación con el encabezado Access-Control-Allow-Origin?

Aquí hay una explicación de mi situación:

Estoy intentando configurar una cookie para una API que se ejecuta localhost:4000en una aplicación web alojada en localhost:3000.

Parece que recibo los encabezados de respuesta correctos en el navegador, pero desafortunadamente no tienen ningún efecto. Estos son los encabezados de respuesta:

HTTP / 1.1 200 OK
Acceso-Control-Permitir-Origen: http: // localhost: 3000
Variar: Origen, Aceptar codificación
Set-Cookie: token = 0d522ba17e130d6d19eb9c25b7ac58387b798639f81ffe75bd449afbc3cc715d6b038e426adeac3316f0511dc7fae3f7; Edad máxima = 86400; Dominio = localhost: 4000; Ruta = /; Caduca = Martes, 19 de septiembre de 2017 21:11:36 GMT; HttpOnly
Tipo de contenido: aplicación / json; juego de caracteres = utf-8
Longitud del contenido: 180
ETag: W / "b4-VNrmF4xNeHGeLrGehNZTQNwAaUQ"
Fecha: lun, 18 de septiembre de 2017 21:11:36 GMT
Conexión: mantener vivo

Además, puedo ver la cookie debajo Response Cookiescuando inspecciono el tráfico usando la pestaña Red de las herramientas de desarrollo de Chrome. Sin embargo, no puedo ver que se establezca una cookie en la pestaña Aplicación debajo Storage/Cookies. No veo ningún error de CORS, así que supongo que me falta algo más.

¿Alguna sugerencia?

Actualización I:

Estoy usando el módulo de solicitud en una aplicación React-Redux para emitir una solicitud a un /signinpunto final en el servidor. Para el servidor utilizo express.

Servidor expreso:

res.cookie ('token', 'xxx-xxx-xxx', {maxAge: 86400000, httpOnly: true, domain: 'localhost: 3000'})

Solicitud en navegador:

request.post ({uri: '/ signin', json: {userName: 'userOne', contraseña: '123456'}}, (err, response, body) => {
    // haciendo cosas
})

Actualización II:

Estoy configurando encabezados de solicitud y respuesta ahora como loco, asegurándome de que estén presentes tanto en la solicitud como en la respuesta. A continuación se muestra una captura de pantalla. Fíjese en las cabeceras Access-Control-Allow-Credentials, Access-Control-Allow-Headers, Access-Control-Allow-Methodsy Access-Control-Allow-Origin. Al observar el problema que encontré en el github de Axios , tengo la impresión de que todos los encabezados obligatorios ya están configurados. Sin embargo, todavía no hay suerte ...

ingrese la descripción de la imagen aquí

Pim Heijden
fuente
4
@PimHeijden eche un vistazo a esto: developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/… tal vez el uso de withCredentials es lo que necesita?
Kalamarico
2
Ok, estás usando request y creo que esta no es la mejor opción, echa un vistazo a esta publicación y la respuesta, axios creo que podría ser útil para ti. stackoverflow.com/questions/39794895/…
Kalamarico
¡Gracias! No me di cuenta de que el requestmódulo no está diseñado para su uso en el navegador. Axios parece haber hecho un gran trabajo hasta ahora. Ahora recibo tanto el encabezado: Access-Control-Allow-Credentials:truecomo Access-Control-Allow-Origin:http://localhost:3000(usado para habilitar CORS). Esto parece correcto, pero el Set-Cookieencabezado no hace nada ...
Pim Heijden
Mismo problema, pero usando directamente Axios: stackoverflow.com/q/43002444/488666 . Si bien de { withCredentials: true }hecho es requerido por el lado de Axios, los encabezados del servidor también deben revisarse cuidadosamente (consulte stackoverflow.com/a/48231372/488666 )
Frosty Z
que encabezados del servidor?
Pim Heijden

Respuestas:

155

Qué necesitas hacer

Para permitir recibir y enviar cookies mediante una solicitud CORS con éxito, haga lo siguiente.

Back-end (servidor): establezca el Access-Control-Allow-Credentialsvalor del encabezado HTTP en true. Además, asegúrese de que los encabezados HTTP Access-Control-Allow-Originy Access-Control-Allow-Headersestén configurados y no con un comodín* .

Para obtener más información sobre cómo configurar CORS en express js, lea los documentos aquí

Front-end (cliente): establezca la XMLHttpRequest.withCredentialsmarca en true, esto se puede lograr de diferentes maneras según la biblioteca de solicitud-respuesta utilizada:

O

Evite tener que usar CORS en combinación con cookies. Puede lograr esto con un proxy.

Si por alguna razón no lo evites. La solución está arriba.

Resultó que Chrome no configurará la cookie si el dominio contiene un puerto. Configurarlo para localhost(sin puerto) no es un problema. ¡Muchas gracias a Erwin por este consejo!

Pim Heijden
fuente
2
Creo que tiene este problema solo por localhostverificar esto aquí: stackoverflow.com/a/1188145 y también esto puede ayudar en su caso ( stackoverflow.com/questions/50966861/… )
Edwin
5
¡Esta respuesta me ayudó mucho! Me tomó mucho tiempo encontrarlo. Pero creo que la respuesta debería mencionar que también se requiere establecer Access-Control-Allow-Originun dominio explícito, no solo "*". Entonces sería la respuesta perfecta
e.dan
6
esta es una buena respuesta, y toda la configuración para CORS, encabezados, backend y front-end, y evitando localhost con anular / etc / hosts localmente con un subdominio real, todavía veo que el cartero muestra un SET-COOKIE en los encabezados de respuesta pero la depuración de Chrome no muestre esto en los encabezados de respuesta y también la cookie no está realmente configurada en Chrome. ¿Alguna otra idea para comprobar?
bjm88
1
@ bjm88 ¿Terminaste descubriendo esto? Estoy exactamente en la misma situación. La cookie está configurada correctamente cuando se conecta desde localhost: 3010 a localhost: 5001 pero no funciona desde localhost: 3010 a fakeremote: 5001 (que apunta a 127.0.0.1 en mi archivo de hosts). Es exactamente lo mismo cuando alojo mi servidor en un servidor real con un dominio personalizado (conectando desde localhost: 3010 a mydomain.com). Hice todo lo que se recomienda en esta respuesta y probé muchas otras cosas.
Formulario
1
En Angular, el cambio requerido del lado del cliente también es agregar withCredentials: fiel a las opciones pasadas a HttpClient.post, .get, options, etc.
Marvin
11

Nota para el navegador Chrome lanzado en 2020.

Una versión futura de Chrome solo entregará cookies con solicitudes entre sitios si están configuradas con SameSite=Noney Secure.

Entonces, si su servidor backend no configura SameSite = None, Chrome usará SameSite = Lax de manera predeterminada y no usará esta cookie con solicitudes {withCredentials: true}.

Más información https://www.chromium.org/updates/same-site .

Los desarrolladores de Firefox y Edge también quieren lanzar esta función en el futuro.

Especificaciones encontradas aquí: https://tools.ietf.org/html/draft-west-cookie-incrementalism-01#page-8

LennyLip
fuente
2
proporcionar samesite = none y la bandera segura requieren HTTPS. ¿Cómo lograr esto en un sistema local donde HTTPS no es una opción? ¿Podemos evitarlo de alguna manera?
nirmal patel
@nirmalpatel Simplemente elimine el valor "Lax" en la consola de desarrollo de Chome.
LennyLip
3

Para que el cliente pueda leer cookies de solicitudes de origen cruzado, debe tener:

  1. Todas las respuestas del servidor deben tener lo siguiente en su encabezado:

    Access-Control-Allow-Credentials: true

  2. El cliente debe enviar todas las solicitudes con la withCredentials: trueopción

En mi implementación con Angular 7 y Spring Boot, lo logré con lo siguiente:


Lado del servidor:

@CrossOrigin(origins = "http://my-cross-origin-url.com", allowCredentials = "true")
@Controller
@RequestMapping(path = "/something")
public class SomethingController {
  ...
}

La origins = "http://my-cross-origin-url.com"parte se agregará Access-Control-Allow-Origin: http://my-cross-origin-url.comal encabezado de respuesta de cada servidor

La allowCredentials = "true"parte se agregará Access-Control-Allow-Credentials: trueal encabezado de respuesta de cada servidor, que es lo que necesitamos para que el cliente lea las cookies.


Lado del cliente:

import { HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler, HttpEvent } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from 'rxjs';

@Injectable()
export class CustomHttpInterceptor implements HttpInterceptor {

    constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // send request with credential options in order to be able to read cross-origin cookies
        req = req.clone({ withCredentials: true });

        // return XSRF-TOKEN in each request's header (anti-CSRF security)
        const headerName = 'X-XSRF-TOKEN';
        let token = this.tokenExtractor.getToken() as string;
        if (token !== null && !req.headers.has(headerName)) {
            req = req.clone({ headers: req.headers.set(headerName, token) });
        }
        return next.handle(req);
    }
}

Con esta clase, en realidad, inyecta cosas adicionales a todas sus solicitudes.

La primera parte req = req.clone({ withCredentials: true });, es lo que necesita para poder enviar cada solicitud con withCredentials: trueopción. Esto prácticamente significa que primero se enviará una solicitud de OPCIÓN, para que usted obtenga sus cookies y el token de autorización entre ellas, antes de enviar las solicitudes POST / PUT / DELETE reales, que necesitan este token adjunto (en el encabezado), en orden para que el servidor verifique y ejecute la solicitud.

La segunda parte es la que maneja específicamente un token anti-CSRF para todas las solicitudes. Lo lee de la cookie cuando es necesario y lo escribe en el encabezado de cada solicitud.

El resultado deseado es algo como esto:

respuesta solicitud

Stefanos Kargas
fuente
¿Qué agrega esta respuesta a la existente?
Pim Heijden
1
Una implementación real. La razón por la que decidí publicarlo es que paso mucho tiempo buscando el mismo problema y agregando piezas de varias publicaciones para darme cuenta. Debería ser mucho más fácil para alguien hacer lo mismo, teniendo esta publicación como comparación.
Stefanos Kargas
Mostrar el ajuste allowCredentials = "true"en la @CrossOriginanotación me ayudó.
ponder275
@lennylip mencionado en su respuesta anterior, muestra un error para samesite y bandera segura. Cómo lograr eso con el servidor localhost sin una bandera segura.
nirmal patel
0

La respuesta de Pim es muy útil. En mi caso, tengo que usar

Expires / Max-Age: "Session"

Si es una fecha y hora, incluso si no está vencida, no enviará la cookie al backend:

Expires / Max-Age: "Thu, 21 May 2020 09:00:34 GMT"

Espero que sea útil para las personas futuras que puedan tener el mismo problema.

Hongbo Miao
fuente
0

Para express, actualice su biblioteca express a 4.17.1la última versión estable. Entonces;

En CorsOption: establezca originsu URL de host local o su URL de producción de frontend y, credentialspor true ejemplo,

  const corsOptions = {
    origin: config.get("origin"),
    credentials: true,
  };

Establecí mi origen dinámicamente usando el módulo config npm .

Luego, en res.cookie:

Para localhost: no es necesario para establecer y sameSite opción segura del todo, se puede establecer httpOnlyque truepara http cookie para Prevenir XSS ataque y otras útiles opciones dependiendo de su caso de uso.

Para el entorno de producción, debe establecer sameSiteen nonepara la solicitud de origen cruzado y secureen true. Recuerde que sameSitefunciona con la última versión express solo como ahora y la última versión de Chrome solo configura las cookies https, por lo tanto, la necesidad de una opción segura.

Así es como hice el mío dinámico

 res
    .cookie("access_token", token, {
      httpOnly: true,
      sameSite: app.get("env") === "development" ? true : "none",
      secure: app.get("env") === "development" ? false : true,
    })
Abdullah Oladipo
fuente