곰터뷰 서비스는 간편한 로그인을 위해 구글 로그인을 지원하기로 했습니다. 구글 로그인같은 소셜 로그인은 대부분 Oauth2 인증 플로우로 진행되는데요. 구글 로그인을 구현하는 방법은 크게 두 가지가 있습니다. 첫 번째는 클라이언트에서 구글 사이트에 접속해 직접 인가 코드를 받아오는 방법이고, 두 번째는 백엔드에서 리다이랙션을 통해 인증, 인가 처리를 모두 담당하는 것입니다. 곰터뷰 서비스에서는 passport를 사용해 백엔드에서 인증 인가 처리를 하는 방법으로 구현했는데요. 이로 인해 수많은 이슈들을 겪게 되었습니다.🥹
이 글에서는 곰터뷰 서비스에 백엔드 인증, 인가 구글 로그인을 도입하면서 겪었던 이슈와 이에 대한 해결 과정에 대해 담고 있습니다. 정말 많이 고생했던 것에 비해 내용 자체는 어렵지 않으니 재미 있게 읽어주시길 바랍니다!
리다이랙션 url 이슈
Note
나중에 겪은 이슈에 비하면 이건 이슈도 아님!
발생한 문제
해결 과정
이 이슈를 처음 만났을 때 가장 먼저 떠오른 해결책은 개발서버와 배포용 서버를 분리하는 것이었습니다. 나중에는 이 두 가지 서버를 꼭 분리할거지만, 현재는 빠르게 구현 진도를 나가고 있는 상황이라서 개발용 서버를 하나 더 구축할 여유가 되지 않았습니다. 게다가 백엔드는 아직 Docker와 CI/CD가 도입되기 전이라서 개발 서버를 한대 더 두는 것은 무리가 있었습니다.
한 대의 서버로 리다이랙션 url 문제를 해결하기 위해 멘토님께 조언도 구해보고 많은 자료도 찾아봤습니다.
해결한 방법
결국 개발용 서버를 한대 더 두는 방법을 사용하기로 했습니다. 이 방법을 선택하게 된 이유는 다음과 같습니다.
- 어차피 언젠가는 개발 서버를 분리해야한다!
- 현재 서버를 한 대만 둬서 해결하기 위해 시간을 쏟고 있는데 이는
서버를 한대만 사용하기 위해 고민하는 시간+나중에 개발 서버를 분리하는 시간까지 두배로 시간이 낭비된다. - Docker와 CI/CD를 도입하면 여러 서버를 운영하는 것이 크게 어렵지 않다. (~~다만 머니 이슈가 있을 뿐~~)
- 확답은 할 수 없지만 로그인 성공 이후 리다이랙션을 처리해주는 것은 백엔드 서버가 아니라 구글이므로 클라이언트 origin을 구분할 방법은 없을 것이다. 있다고 하더라도 안티 패턴일 것!
따라서 곰터뷰 서비스는 서버를 한대 더 확장하는 방식을 사용하기로 결정했습니다.
브라우저 쿠키 정책 문제
이 이슈가 아마 구글 로그인 도입의 메인 이벤트!! 입니다😂 위에서 보신 이미지처럼 곰터뷰 서비스는 구글 로그인 성공시 set-cookie 헤더를 통해 accessToken응답을 받고있었습니다. accessToken의 값을 쿠키로 관리하기로 한 이유는 다음과 같습니다.
- 쿠키 설정을 httpOnly로 하게 되면 클라이언트에서 자바스크립트를 통해 조작할 수 없어서 보안을 강화할 수 있습니다.
- 브라우저에서는 쿠키의 도메인과 동일한 서버로 요청을 보낼 때 마다 헤더에 자동으로 쿠키를 포함해줍니다. 따라서 프론트엔드에서 처리해야할 로그인 로직이 간단해집니다.
- SameSite 옵션을 설정해서 쿠키가 크로스 사이트 요청에 포함되는 것을 방지할 수 있습니다.
이렇게 쿠키의 장점만 보고 쿠키를 도입했는데 개발 환경에서는 아주 골칫덩어리가 되어버렸습니다. 지금부터 개발 환경에서 토큰을 쿠키로 관리하는 것이 얼마나 힘든지 함께 알아봅시다.
쿠키의 SameSite 정책이란?
위에서 언급한 것 처럼 쿠키의 SameSite 옵션을 설정해서 CSRF(크로스 사이트 요청 위조)공격을 방어할 수 있는데요. 현재 SameSite 정책은 아래 세 가지 옵션이 있습니다.
Strict
이 정책이 설정된 쿠키는 오직 동일한 사이트로의 요청에만 전송됩니다. 여기서 동일 사이트란 프로토콜, 포트(명시된 경우), 호스트가 같은 두 주소를 말합니다.
예를들어 api.gomterview.com과 www.gomterview.com은 동일한 호스트 네임을 가졌으니 동일한 사이트죠. 하지만 api.gomterview.com과 localhost: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라는 쿠키의 도메인을 변경할 수 있는 옵션을 제공합니다.
nginx proxy를 설정한다면
클라이언트에서 인가코드를 발급받는 방법을 고려해보자
이 방법의 장점
- 클라이언트와 백엔드가 api 요청을 통해 응답을 주고받기때문에 프록시에서 적절한 처리를 해줄 수 있습니다.
- 백엔드 서버에서 헤더를 통해 토큰을 발급하기 때문에 쿠키 정책으로 고민할 필요도 없었습니다.
이 방법을 선택하지 않은 이유
- 클라이언트에서 oauth 관련 키를 관리해야합니다
- 로그인 관련 백엔드 로직이 모두 완료된 상태라서 전부 바꾸기에는 리스크가 컸습니다.
이 방법을 사용함으로써 얻을 수 있는 장점은 지금까지 겪었던 문제들을 바로 해결할 수 있지만 단점이 너무 치명적이라서 채택할 수 없었습니다.
SameSite=None
사실 이 방법을 사용하면 바로 해결할 수 있는 문제이긴 합니다. 배포된 서버는 이미 https가 설정되어 있었기 때문에 클라이언트 개발 환경에만 https를 설정해주면 모든게 해결됩니다. 그렇지만 이 방법은 사용하지 않았습니다. 클라이언트 개발자 3명의 컴퓨터에 모두 https 설정을 해야 한다는 것이 매우 번거로운 작업이라고 생각했기 때문입니다. 로컬 환경의 https 설정에 대한 가이드를 작성해서 공유한 후 팀원들이 직접 개발 환경 을 설정하거나, 팀원들과 만나는 날 이 내용에 대해 안내해줘야 하는데 중요도가 크지 않은 로그인 작업에 모두가 에너지를 투자하는 것은 비효율적이라고 생각했기 때문입니다. 다른 팀원분들이 최대한 바로 사용할 수 있는 해결책을 찾고싶었기 때문에 이 해결책도 기각했습니다.
그래서 해결한 방법은?
위에서 고민했던 거창한 해결책들에 비해 아주 간단한 트릭으로 이 문제를 해결했는데요. 바로 개발용 token 발급 api를 하나 만드는 것이었습니다. 랜딩 화면의 구글 로그인 버튼의 동작을 env에 따라서 다르게 동작하도록 구성해서 개발 모드일때는 아래와 같은 흐름으로 쿠키가 설정되게 했습니다.
마무리
지금까지 Oauth2 로그인을 구현해본 경험이 있었고, 구글 로그인은 레퍼런스도 많기 때문에 로그인 구현에 대한 Jira 티켓을 3시간으로 예상했었습니다. 지금 보니 정말 헛웃음이 나오네요😂😂
Info
제 2주간의 고민으로는 현재 방법이 최선의 해결책이라고 생각합니다! 하지만 이보다 더 나은 해 결책이 있거나 궁금한 내용이 있으시면 언제든지 댓글로 피드백 부탁드립니다😊