서버와 클라이언트의 통신을 이용한 로그인 기능을 구현하는 것에 쿠키를 사용하기로 했다.
왜냐하면 httpOnly 쿠키 헤더를 활성화하면 XSS 공격은 어느 정도 방어가 가능하고, 쿠키는 클라이언트에서 코드로 조작하는 등의 수고로움이 없기 때문이다.
그런데 CORS의 세계는 너무나 냉혹했다. CORS 정책을 따르지 않는 쿠키는 HTTP 통신을 할 수 없었다.
지금부터 3일 밤낮을 헤매고 4일째 아침에 찾은 답을 포스팅하겠다.
일단 쿠키 헤더의 Same-Site 속성에 대해 알아보자.
Same-Site 속성은 다른 도메인 간의 통신에 대한 보안에 대한 설정이고, 3가지 설정값이 있다.
내 프로젝트의 HTTP 통신에서 /login
요청은 POST이고, 요청의 응답에는 token이 쿠키로 포함되어야 한다. 그래서 Same-Site는 None이어야 하고, Secure를 활성화 한다.
하지만 개발을 하는 localhost는 기본적으로 HTTP 프로토콜을 사용하기 때문에, localhost의 프로토콜을 HTTPS로 바꿔야했다.
How to use HTTPS for local development 포스트를 참고함으로써 이 문제는 해결했다.
두 번째는 CORS 통신에 포함 할 쿠키에 관한 설정이다. 쿠키는 요청을 할 때에 자동으로 요청에 포함되지만, CORS 요청에서는 아니다.
클라이언트의 XHR 객체에는 withCredentials이라는 옵션이 있다. (XHR.withCredentials 문서)
이는 쿠키, Authorization header 같은 user Credentials를 요청에 포함할 것인지에 대한 설정이다. true로 설정하면 user Credentials를 요청에 포함할 수 있다.
하지만 이대로만 요청을 보내면 Request error가 발생한다.
에러 로그를 읽어보면 Credentials request에 대한 Response의 헤더에 Access-Control-Allow-Credentials
가 true
여야 한다고 한다.
나는 express를 사용하고 있으니 express 기준으로 설명하겠다.
express cors middleware 문서의 config option 문단을 보면 해당 옵션에 대한 설명이 있다.
const express = require("express");
const server = express();
const cors = require("cors");
...
server
.use(cors({ origin: true, credentials: true }))
.use(express.json())
.use(cookieParser());
...
이렇게 설정해주면 해당 서버의 모든 응답에 Access-Control-Allow-Credentials
헤더가 붙는다.
cors의 config 객체를 보면 origin: true
도 설정해줬는데, 이유는 다음과 같다.
Access-Control-Allow-Credentials
가 true
인 응답은 반드시 Access-Control-Allow-Origin
이 명시되어 있어야 한다. 와일드카드( * )는 사용할 수 없고, origin을 명시해야한다.
cors cofing의 origin
을 true
로 설정하면 요청을 보내온 origin이 Access-Control-Allow-Origin
의 값으로 설정된다.
이 모든 문제가 해결되었다면, 이렇게 쿠키가 포함된 응답을 받을 수가 있다.
여기까지 내가 이 문제를 접한지 1일차에 해결한 부분이다.
그런데 나는 Chrome 개발자 도구의 Application 탭의 Cookies storage에는 저장된 쿠키가 안 보이는데, 왜 이러는지 모르겠다.
거기에 나는 쿠키가 필요한 API에 요청을 보냈는데, 자꾸 에러가 떴다.
서버에 요청은 제대로 갔는데, refreshToken 쿠키가 없다고 에러를 던지고 있다. 이것 때문에 3일 밤낮을 헤맸다.
그리고 해결방법은 어처구니 없게 간단했고, 나는 어처구니 없게 멍청했다.
...
const requestRefresh = async () => {
const response = await axios.get(
"쿠키가.필요한/API/EndPoint"
);
console.log(response);
};
...
이게 쿠키가 필요한 API로 보내는 요청 코드였는데, 여기에도 withCredentials
옵션이 필요했다.
그러니까, 쿠키를 받을 요청 뿐만 아니라 쿠키를 보낼 요청 또한 withCredentials 옵션을 true로 설정해야한다!
아니, 지금 와서 생각해보면 분명히 이치에 맞는 말이다.
하지만 그 당시에는 Application의 Cookie Storage에 쿠키가 나타나지 않아서 쿠키를 제대로 못 받아와서 생긴 일이라고 생각했다. 그래서 해결이 더뎠다.
그러니까 위의 요청에 withCredentials
옵션을 true
로 설정하면..
...
const requestRefresh = async () => {
const response = await axios.get(
"쿠키가.필요한/API/EndPoint",
{
withCredentials: true
}
);
console.log(response);
};
...
뭐야... 잘 되잖아...
클라이언트
쿠키가 필요한 모든 요청에 withCertification 옵션 활성화( 쿠키를 받는 요청에도, 주는 요청에도 모두 )
https 프로토콜 사용해야 함 ( Secure 쿠키는 HTTPS 간의 통신에만 전송 돼서 )
서버
응답 Access-Control-Allow-Credentials 헤더 활성화
응답 Access-Control-Allow-Origin 헤더에 도메인 명시( 와일드 카드 사용 불가 )
쿠키 헤더에 Same-Site: "None" 설정( 다른 Origin간의 통신에도 전송되게 ) , Secure 설정( Same-Site가 None이면 Secure 활성화 되어야함 )
그래도 시간 들이고 해결 못 하는 것 보단 해결을 한 것이 낫다. 어쨌든 내가 이겼다.
그리고 오류를 해결하면서 HTTP 통신과 쿠키, 뭐 이런 것들에 대해서도 여러가지 배웠고, CORS 이슈에 대해 서버와 클라이언트 둘 다 관심을 기울여야 한다는 것을 느꼈다. 실제로도 그렇고.
나는 그냥 로그인 기능이 있는 CRUD 리액트 웹앱을 만들고 싶었는데, 생각 이상으로, 너무나도 생각 이상으로 알아야 할 것이 많다. 배우는 것이 많아서 좋기는 하다.
덕분에 잘 이해하고갑니다. 감사합니다 !!