📁 개발

Oauth2를 도입하신다고요? 꼭 쿠키 사용하세요 ^^


곰터뷰 서비스는 간편한 로그인을 위해 구글 로그인을 지원하기로 했습니다. 구글 로그인같은 소셜 로그인은 대부분 Oauth2 인증 플로우로 진행되는데요. 구글 로그인을 구현하는 방법은 크게 두 가지가 있습니다. 첫 번째는 클라이언트에서 구글 사이트에 접속해 직접 인가 코드를 받아오는 방법이고, 두 번째는 백엔드에서 리다이랙션을 통해 인증, 인가 처리를 모두 담당하는 것입니다. 곰터뷰 서비스에서는 passport를 사용해 백엔드에서 인증 인가 처리를 하는 방법으로 구현했는데요. 이로 인해 수많은 이슈들을 겪게 되었습니다.🥹

이 글에서는 곰터뷰 서비스에 백엔드 인증, 인가 구글 로그인을 도입하면서 겪었던 이슈와 이에 대한 해결 과정에 대해 담고 있습니다. 정말 많이 고생했던 것에 비해 내용 자체는 어렵지 않으니 재미있게 읽어주시길 바랍니다!

리다이랙션 url 이슈

Note

나중에 겪은 이슈에 비하면 이건 이슈도 아님!

발생한 문제

백엔드 인증, 인가 로그인 플로우 구글 로그인은 위와 같은 플로우로 구성되어 있었는데요. 여기서 가장 큰 이슈는 8번 항목인 리다이랙션 url이었습니다. 배포된 곰터뷰 웹페이지에서는 로그인 성공시 gomterview.com/mypage로 리다이랙션 되야 했지만, 개발 환경에서는 localhost:3000/mypage로 이동해야 했습니다. 아직은 우리 서비스에 유저가 없어서 문제가 없지만, 훗날 유저가 있는 상태에서 서비스하려면 개발 환경에서 로그인을 테스트할 때 마다 서버의 설정을 바꿔줘야 한다는 문제가 있었습니다.

해결 과정

이 이슈를 처음 만났을 때 가장 먼저 떠오른 해결책은 개발서버와 배포용 서버를 분리하는 것이었습니다. 나중에는 이 두 가지 서버를 꼭 분리할거지만, 현재는 빠르게 구현 진도를 나가고 있는 상황이라서 개발용 서버를 하나 더 구축할 여유가 되지 않았습니다. 게다가 백엔드는 아직 Docker와 CI/CD가 도입되기 전이라서 개발 서버를 한대 더 두는 것은 무리가 있었습니다. 한 대의 서버로 리다이랙션 url 문제를 해결하기 위해 멘토님께 조언도 구해보고 많은 자료도 찾아봤습니다. 멘토님께 질문했던 내용!

해결한 방법

결국 개발용 서버를 한대 더 두는 방법을 사용하기로 했습니다. 이 방법을 선택하게 된 이유는 다음과 같습니다.

따라서 곰터뷰 서비스는 서버를 한대 더 확장하는 방식을 사용하기로 결정했습니다.

브라우저 쿠키 정책 문제

이 이슈가 아마 구글 로그인 도입의 메인 이벤트!! 입니다😂 위에서 보신 이미지처럼 곰터뷰 서비스는 구글 로그인 성공시 set-cookie 헤더를 통해 accessToken응답을 받고있었습니다. accessToken의 값을 쿠키로 관리하기로 한 이유는 다음과 같습니다.

이렇게 쿠키의 장점만 보고 쿠키를 도입했는데 개발 환경에서는 아주 골칫덩어리가 되어버렸습니다. 지금부터 개발 환경에서 토큰을 쿠키로 관리하는 것이 얼마나 힘든지 함께 알아봅시다.

쿠키의 SameSite 정책이란?

위에서 언급한 것 처럼 쿠키의 SameSite 옵션을 설정해서 CSRF(크로스 사이트 요청 위조)공격을 방어할 수 있는데요. 현재 SameSite 정책은 아래 세 가지 옵션이 있습니다.

Strict

이 정책이 설정된 쿠키는 오직 동일한 사이트로의 요청에만 전송됩니다. 여기서 동일 사이트란 프로토콜, 포트(명시된 경우), 호스트가 같은 두 주소를 말합니다. 예를들어 api.gomterview.comwww.gomterview.com은 동일한 호스트 네임을 가졌으니 동일한 사이트죠. 하지만 api.gomterview.comlocalhost:3000은 호스트가 다르기 때문에 다른 사이트로 취급됩니다.

Lax (기본값)

탑 레벨 탐색에 대한 요청에만 쿠키가 전송됩니다. 탑 레벨 탐색이란 링크를 통해 이동하는 것을 말하는데요. 예시를 들어 설명하자면 window.location.href를 통한 링크 이동에 의한 GET 요청이 이에 해당합니다. 때문에 이 정책을 가진 쿠키는 axios나 fetch 등을 통해 api 요청을 할 때는 쿠키가 전송되지 않습니다.

None

모든 크로스 사이트 요청에 대해 전송됩니다. 단, SameSite=None 정책 쿠키는 반드시 Secure 플래그와 함께 사용되어야 하는데요. Secure 플래스를 설정한 쿠키는 https를 통해서만 전송될 수 있습니다. 만약 로컬호스트에서 배포된 서버에 로그인을 테스트하려면 로컬호스트도 https 프로토콜을 설정해야 합니다.

곰터뷰는 어떤 쿠키 정책을 사용했을까?

사실 배포된 곰터뷰 서비스에서는 쿠키에 대한 문제를 전혀 걱정할 필요가 없습니다. 왜냐하면 둘다 gomterview.com 이라는 도메인에서 DNS 레코드만 분리해서 사용하기 때문에 브라우저상에서 동일한 출처로 취급되기 때문이죠. 하지만 쿠키에 대한 문제는 언제나 개발 환경에 대한 이야기입니다! 개발 환경에서는 어떨까요? 배포 서버는 gomterview.com 도메인을 사용하지만, 웹사이트는 localhost 호스트를 사용합니다. 따라서 이 둘은 동일 출처가 아니죠. 때문에 이에 대한 대응을 해줘야합니다. 다행히 현재 구글 로그인은 api 요청이 아니라 링크 이동을 통한 상위 탐색으로 진행하기 때문에 SameSite의 기본값인 lax로 설정해도 백엔드의 쿠키가 클라이언트에 잘 설정되었습니다. 그래서 곰터뷰는 SameSite=Lax의 쿠키 정책을 사용하기로 결정했습니다.

잘 설정되는데 뭐가 문제야?

쿠키의 도메인이 gomterview.com으로 설정되어있음

리다이랙션도 잘 수행되고, 쿠키도 잘 설정되고 모든게 순조롭게 진행될 줄 알았습니다. 하지만 가장 큰 이슈가 기다리고 있었는데요. 바로 localhost로 부터 발생한 api 요청에 쿠키가 자동으로 포함되지 않는다는 것이었습니다. 왜냐하면 쿠키의 domain 설정이 .gomterview.com으로 되어있었기 때문이죠. 브라우저에서는 모든 요청에 쿠키를 포함하는 것이 아니라 쿠키의 도메인 값과 현재 api 요청을 보내는 주소가 일치할 때만 요청에 쿠키를 포함시킵니다. 그리고 곰터뷰 서비스의 쿠키는 SameSite=Lax라서 크로스 도메인간 상위 탐색이 아닌 요청에 포함시킬 수 없습니다.

쿠키 문제를 해결하기 위해 시도했던 것들

만약 쿠키의 도메인 설정을 localhost로 설정한다면?

쿠키의 도메인 설정이 localhost라면 localhost -> localhost로의 요청에는 쿠키가 자동으로 포함됩니다. 그래서 Webpack dev server의 proxy를 사용해서 요청을 localhost로 바꾸면 쿠키가 잘 설정될 것이라는 가설을 세우고 실험을 해봤습니다. 프록시를 설정했을 때

Tip

여기서 짚고 넘어갈 점! localhost 웹팩 프록시부터 서버까지는 크로스 도메인인데 어떻게 쿠키가 전송될 수 있을까요? 바로 쿠키와 CORS 정책은 브라우저에서 제한하는 것이기 때문입니다. 클라이언트 사이트부터 웹팩 프록시까지 가는 요청은 브라우저의 정책을 따라야하지만, 웹팩 프록시부터 실제 서버까지의 요청은 서버와 서버간의 요청이므로 브라우저의 정책을 따르지 않습니다. 따라서 크로스 도메인간에도 쿠키가 전송됩니다.

하지만 이 방법에도 문제가 있었는데요. 쿠키의 도메인이 localhost가 되면 서버에서 set-cookie 헤더를브라우저에 쿠키가 설정되지 않는다는 것입니다. 왜냐하면 동일 출처 정책(Same-Origin Policy)으로 인해 다른 출처의 쿠키를 설정할 수 없기 때문입니다. 때문에 이 방법도 사용할 수 없었습니다.

Webpack dev Server Proxy에서 쿠키 도메인을 변경한다면?

웹팩 dev server의 프록시는 http-proxy-middleware를 사용합니다. 이 미들웨어에서는 cookieDomainRewrite라는 쿠키의 도메인을 변경할 수 있는 옵션을 제공합니다. 쿠키 도메인 변경 옵션 하지만 이 옵션도 사용할 수 없었습니다. 왜냐하면 서버에서 set-cookie 응답을 내려주는 것은 클라이언트의 api 요청에 대한 것이 아니라 링크 이동을 통한 상위탐색에 대한 응답이므로 프록시를 통할 수 없습니다. 따라서 이 방법으로도 해결할 수 없었습니다.

nginx proxy를 설정한다면

nginx 프록시에서 쿠키 도메인을 바꿔주는 경우 nginx로 리버스 프록시를 설정해서 쿠키 도메인 gomterview -> localhost로 변경해준 후 웹팩 프록시로 응답을 내려준다면 쿠키가 설정되는 것도 문제 없고, 쿠키가 api 요청에 포함돼서 보내지는 것에도 문제가 없다고 생각했습니다. 하지만 이 방법 또한 통하지 않았습니다. 왜냐하면 로그인 응답이 리다이랙션 응답이라서 웹팩 프록시를 통해 받을 수 없기 때문입니다.😭😭 nginx 프록시에서 쿠키 도메인을 변경해주더라도 리다이랙션 응답을 통해 설정되는 쿠키는 브라우저를 통하기 때문에 브라우저 정책을 피할 수 없었습니다. 이 가설을 세우게 된 스토리😂😂

클라이언트에서 인가코드를 발급받는 방법을 고려해보자

클라이언트에서 인가 코드 발급받기 위 플로우로 클라이언트에서 인가 코드를 받아온 후 백엔드에 전달하는 방식에 대해 고려해봤습니다.

이 방법의 장점
이 방법을 선택하지 않은 이유

이 방법을 사용함으로써 얻을 수 있는 장점은 지금까지 겪었던 문제들을 바로 해결할 수 있지만 단점이 너무 치명적이라서 채택할 수 없었습니다.

SameSite=None

사실 이 방법을 사용하면 바로 해결할 수 있는 문제이긴 합니다. 배포된 서버는 이미 https가 설정되어 있었기 때문에 클라이언트 개발 환경에만 https를 설정해주면 모든게 해결됩니다. 그렇지만 이 방법은 사용하지 않았습니다. 클라이언트 개발자 3명의 컴퓨터에 모두 https 설정을 해야 한다는 것이 매우 번거로운 작업이라고 생각했기 때문입니다. 로컬 환경의 https 설정에 대한 가이드를 작성해서 공유한 후 팀원들이 직접 개발 환경을 설정하거나, 팀원들과 만나는 날 이 내용에 대해 안내해줘야 하는데 중요도가 크지 않은 로그인 작업에 모두가 에너지를 투자하는 것은 비효율적이라고 생각했기 때문입니다. 다른 팀원분들이 최대한 바로 사용할 수 있는 해결책을 찾고싶었기 때문에 이 해결책도 기각했습니다.

그래서 해결한 방법은?

위에서 고민했던 거창한 해결책들에 비해 아주 간단한 트릭으로 이 문제를 해결했는데요. 바로 개발용 token 발급 api를 하나 만드는 것이었습니다. 랜딩 화면의 구글 로그인 버튼의 동작을 env에 따라서 다르게 동작하도록 구성해서 개발 모드일때는 아래와 같은 흐름으로 쿠키가 설정되게 했습니다. 개발용 토큰 발급 로직 개발 모드일 때 토큰 발급 요청을 서버에 보내면 서버는 Authorization Header를 통해 accessToken을 발급해줍니다. Authorization Header를 사용하면 더이상 쿠키가 아니기 때문에 크로스 도메인에 대한 고려를 하지 않아도 됩니다. 하지만 클라이언트에서 매 요청마다 쿠키를 직접 헤더에 넣어야 한다는 불편함이 있었죠. 그래서 클라이언트상에서 accessToken을 localhost 도메인의 쿠키로 직접 설정해주었습니다. 그리고 이 값이 매 요청에 자동으로 포함되도록 웹팩 프록시를 설정했습니다. 이 방법을 사용하니 기존의 api 요청 함수를 수정하지 않고, 구글 로그인을 보내는 버튼에만 로직을 추가해서 개발환경의 로그인을 구현할 수 있었습니다.

마무리

지금까지 Oauth2 로그인을 구현해본 경험이 있었고, 구글 로그인은 레퍼런스도 많기 때문에 로그인 구현에 대한 Jira 티켓을 3시간으로 예상했었습니다. 지금 보니 정말 헛웃음이 나오네요😂😂 로그인 구현에 대한 Jira 티켓 생각해보면 지금까지 제가 구현했던 로그인은 모두 Authorization 헤더를 사용했었습니다. 때문에 이렇게 쿠키와 브라우저 정책에 대해서 깊게 고민할 필요가 없었죠. 그래서 이번 프로젝트에서 쿠키를 사용한 로그인을 구현하면서 딜레마에 빠지기도 했습니다. '이미 알고있는 쉽고 간단한 방법으로 구현할까? 아니야 그래도 이만큼 고민했는데 포기할수는 없지' 이 두 가지 생각이 계속 머릿속에서 떠나지 않았습니다. 사실 이 작업보다 중요한 작업은 많고, 쿠키 문제를 해결하기 위해 Docker, nginx, reverse proxy 등에 대해 학습하고 적용해보는 것이 프론트엔드 개발자로써 쌓아야 할 역량에서는 좀 벗어났다는 생각도 들었습니다. 그치만 포기하기엔 너무 많은 길을 와버렸다...! 그래서 하루에 2시간 이상 고민하지 말자는 나만의 규칙을 세우고 다른 작업과 병행하면서 문제를 해결해보려고 노력했습니다. 거창하게 고민한 것에 비해 간단한 방법으로 문제를 해결하게 돼서 약간 허무한 감도 있지만, 해결책을 찾는 과정동안 정말 머리를 싸매며 몰두하게 돼서 재밌기도 했습니다. 이제 누가 제게 쿠키에 대해 묻는다면 아주 당당하게 설명해줄 자신감이 생겼습니다!!

Info

제 2주간의 고민으로는 현재 방법이 최선의 해결책이라고 생각합니다! 하지만 이보다 더 나은 해결책이 있거나 궁금한 내용이 있으시면 언제든지 댓글로 피드백 부탁드립니다😊

관련 PR