Definir cookies para solicitações de origem cruzada

102

Como compartilhar cookies de origem cruzada? Mais especificamente, como usar o Set-Cookiecabeçalho em combinação com o cabeçalho Access-Control-Allow-Origin?

Aqui está uma explicação da minha situação:

Estou tentando definir um cookie para uma API que está sendo executada localhost:4000em um aplicativo da web que está hospedado em localhost:3000.

Parece que estou recebendo os cabeçalhos de resposta corretos no navegador, mas infelizmente eles não surtem efeito. Estes são os cabeçalhos de resposta:

HTTP / 1.1 200 OK
Access-Control-Allow-Origin: http: // localhost: 3000
Variar: Origem, Aceitar-Codificação
Set-Cookie: token = 0d522ba17e130d6d19eb9c25b7ac58387b798639f81ffe75bd449afbc3cc715d6b038e426adeac3316f0511dc7fae3f7; Max-Age = 86400; Domínio = localhost: 4000; Caminho = /; Expira = terça, 19 de setembro de 2017 21:11:36 GMT; HttpOnly
Tipo de conteúdo: aplicativo / json; charset = utf-8
Comprimento do conteúdo: 180
ETag: W / "b4-VNrmF4xNeHGeLrGehNZTQNwAaUQ"
Data: Seg, 18 de setembro de 2017 21:11:36 GMT
Conexão: keep-alive

Além disso, posso ver o cookie Response Cookiesquando inspeciono o tráfego usando a guia Rede das ferramentas de desenvolvedor do Chrome. Ainda assim, não consigo ver um cookie sendo definido na guia Aplicativo em Storage/Cookies. Não vejo nenhum erro CORS, então presumo que esteja faltando algo mais.

Alguma sugestão?

Atualização I:

Estou usando o módulo de solicitação em um aplicativo React-Redux para emitir uma solicitação para um /signinendpoint no servidor. Para o servidor eu uso express.

Servidor expresso:

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

Solicitação no navegador:

request.post ({uri: '/ signin', json: {userName: 'userOne', senha: '123456'}}, (err, resposta, corpo) => {
    // fazendo coisas
})

Atualização II:

Estou definindo cabeçalhos de solicitação e resposta como um louco, certificando-me de que eles estejam presentes tanto na solicitação quanto na resposta. Abaixo está uma captura de tela. Observe os cabeçalhos Access-Control-Allow-Credentials, Access-Control-Allow-Headers, Access-Control-Allow-Methodse Access-Control-Allow-Origin. Olhando para o problema que encontrei no github do Axios , tenho a impressão de que todos os cabeçalhos necessários agora estão definidos. No entanto, ainda não há sorte ...

insira a descrição da imagem aqui

Pim Heijden
fonte
4
@PimHeijden dê uma olhada nisso: developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/… talvez o uso de withCredentials seja o que você precisa?
Kalamarico
2
Ok, você está usando request e acho que essa não é a melhor escolha, dê uma olhada neste post e na resposta, axios eu acho que podem ser úteis para você. stackoverflow.com/questions/39794895/…
Kalamarico
Obrigado! Não percebi que o requestmódulo não se destina ao uso no navegador. Axios parece ter feito um ótimo trabalho até agora. Recebo agora o cabeçalho: Access-Control-Allow-Credentials:truee Access-Control-Allow-Origin:http://localhost:3000(usado para habilitar o CORS). Isso parece certo, mas o Set-Cookiecabeçalho não faz nada ...
Pim Heijden
Mesmo problema, mas usando diretamente Axios: stackoverflow.com/q/43002444/488666 . Embora { withCredentials: true }seja de fato exigido pelo lado do Axios, os cabeçalhos do servidor também devem ser verificados com cuidado (consulte stackoverflow.com/a/48231372/488666 )
Frosty Z de
quais cabeçalhos de servidor?
Pim Heijden

Respostas:

169

O que você precisa fazer

Para permitir o recebimento e envio de cookies por uma solicitação CORS com sucesso, faça o seguinte.

Back-end (servidor): Defina o Access-Control-Allow-Credentialsvalor do cabeçalho HTTP como true. Além disso, certifique-se de que os cabeçalhos HTTP Access-Control-Allow-Origine Access-Control-Allow-Headersestejam definidos e não com um caractere curinga* .

Para obter mais informações sobre como configurar CORS em express js, leia os documentos aqui

Front-end (cliente): defina o XMLHttpRequest.withCredentialssinalizador como true, isso pode ser feito de diferentes maneiras, dependendo da biblioteca de solicitação-resposta usada:

Ou

Evite usar CORS em combinação com cookies. Você pode fazer isso com um proxy.

Se você por algum motivo, não evite. A solução está acima.

Descobriu-se que o Chrome não definirá o cookie se o domínio contiver uma porta. Configurá-lo para localhost(sem porta) não é um problema. Muito obrigado ao Erwin por esta dica!

Pim Heijden
fonte
2
Acho que você tem esse problema só por causa da localhostchecagem aqui: stackoverflow.com/a/1188145 e também isso pode ajudar no seu caso ( stackoverflow.com/questions/50966861/… )
Edwin
5
Essa resposta me ajudou muito! Demorou muito para encontrá-lo. Mas acho que a resposta deve mencionar que a configuração Access-Control-Allow-Originde um domínio explícito, não apenas, "*"também é necessária. Então seria a resposta perfeita
e.dan
6
esta é uma boa resposta, e toda a configuração para CORS, cabeçalhos, back-end e front end, e evitando localhost com substituir / etc / hosts localmente por um subdomínio real, ainda vejo que o postman mostra um SET-COOKIE nos cabeçalhos de resposta, mas a depuração do Chrome não mostre isso nos cabeçalhos de resposta e também o cookie não está realmente definido no cromo. Alguma outra ideia para verificar?
bjm88 de
1
@ bjm88 Você acabou descobrindo isso? Estou exatamente na mesma situação. O cookie é definido corretamente ao conectar de localhost: 3010 para localhost: 5001, mas não funciona de localhost: 3010 para fakeremote: 5001 (que aponta para 127.0.0.1 em meu arquivo hosts). É exatamente o mesmo quando eu hospedo meu servidor em um servidor real com um domínio personalizado (conectando de localhost: 3010 a mydomain.com). Fiz tudo o que é recomendado nesta resposta e tentei muitas outras coisas.
Formulário de
1
No Angular, a alteração necessária do lado do cliente também é adicionar withCredentials: true para as opções passadas para HttpClient.post, .get, options, etc.
Marvin
18

Nota para o navegador Chrome lançado em 2020.

Uma versão futura do Chrome só entregará cookies com solicitações entre sites se eles forem configurados com SameSite=Nonee Secure.

Portanto, se seu servidor de back-end não definir SameSite = None, o Chrome usará SameSite = Lax por padrão e não usará este cookie com solicitações {withCredentials: true}.

Mais informações https://www.chromium.org/updates/same-site .

Os desenvolvedores do Firefox e do Edge também desejam lançar esse recurso no futuro.

Especificação encontrada aqui: https://tools.ietf.org/html/draft-west-cookie-incrementalism-01#page-8

LennyLip
fonte
4
fornecer samesite = none e sinalizador seguro requerem HTTPS. Como fazer isso em um sistema local onde HTTPS não é uma opção? podemos contornar de alguma forma?
nirmal patel
@nirmalpatel Apenas remova o valor "Lax" no console dev Chome.
LennyLip,
5

Para que o cliente possa ler cookies de solicitações de origem cruzada, você precisa ter:

  1. Todas as respostas do servidor precisam ter o seguinte em seu cabeçalho:

    Access-Control-Allow-Credentials: true

  2. O cliente precisa enviar todas as solicitações com withCredentials: trueopção

Em minha implementação com Angular 7 e Spring Boot, consegui isso com o seguinte:


Lado do servidor:

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

A origins = "http://my-cross-origin-url.com"parte será adicionada Access-Control-Allow-Origin: http://my-cross-origin-url.comao cabeçalho de resposta de cada servidor

A allowCredentials = "true"parte será adicionada Access-Control-Allow-Credentials: trueao cabeçalho de resposta de cada servidor, que é o que precisamos para que o cliente leia os cookies


Lado do 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);
    }
}

Com essa classe, você injeta material adicional em todas as suas solicitações.

A primeira parte req = req.clone({ withCredentials: true });é o que você precisa para enviar cada solicitação com withCredentials: trueopção. Isso praticamente significa que uma solicitação OPTION será enviada primeiro, para que você obtenha seus cookies e o token de autorização entre eles, antes de enviar as solicitações POST / PUT / DELETE reais, que precisam deste token anexado a elas (no cabeçalho), em pedido para o servidor verificar e executar a solicitação.

A segunda parte é aquela que trata especificamente de um token anti-CSRF para todas as solicitações. Lê-o do cookie quando necessário e o grava no cabeçalho de cada solicitação.

O resultado desejado é mais ou menos assim:

resposta solicitação

Stefanos Kargas
fonte
o que esta resposta adiciona à existente?
Pim Heijden
2
Uma implementação real. A razão pela qual decidi postá-lo é que passo muito tempo procurando o mesmo problema e juntando peças de várias postagens para percebê-lo. Deve ser muito mais fácil para alguém fazer o mesmo, tendo este post como comparação.
Stefanos Kargas
Mostrar configuração allowCredentials = "true"na @CrossOriginanotação me ajudou.
ponder275
@lennylip mencionado em sua resposta acima, está mostrando erro para o mesmo site e sinalizador seguro. Como fazer isso com o servidor localhost sem um sinalizador seguro.
nirmal patel
2

Para expresso, atualize sua biblioteca expressa para 4.17.1 a versão estável mais recente. Então;

Em CorsOption: Defina origincomo seu localhost url ou seu URL de produção de frontend e, credentialspor true exemplo,

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

Eu defino minha origem dinamicamente usando o módulo config npm .

Então, em res.cookie:

Para localhost: você não precisa definir sameSite e opção de seguro em tudo, você pode definir httpOnlypara truepara http cookie para ataque prevent XSS e outras úteis opções , dependendo do seu caso de uso.

Para o ambiente de produção, você precisa definir sameSitecomo nonepara solicitação de origem cruzada e securecomo true. Lembre-se de sameSitefuncionar com a versão mais recente expressa apenas como agora e a versão mais recente do Chrome configurada apenas com o cookie https, portanto, a necessidade de uma opção segura.

Aqui está como tornei o meu 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
fonte
0

A resposta de Pim é muito útil. No meu caso, tenho que usar

Expires / Max-Age: "Session"

Se for um dateTime, mesmo que não tenha expirado, ele ainda não enviará o cookie para o backend:

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

Espero que seja útil para futuras pessoas que possam encontrar o mesmo problema.

Hongbo Miao
fonte