Skip to main content

본격 HTTP 쿠키 삽질기

· 14 min read ·
yoonhoGo

쿠키가 마녀에게 잡혀 있던 이유가 있었다.

오븐브레이크가 아니라 멘탈브레이크였다.

쿠키런때부터 인성 알아봤다.(인성 문제있어?)

그렇게 탈출을 도와주려고 했는데... 쿠키 넌 개인주의야

intro.

거진 2-3주에 걸친 쿠키 삽질기를 녹여낸 글입니다. 브라우저에서 쿠키를 사용할 때 엄청 애를 먹었습니다. stack overflow나 MDN를 읽는 것만으로 알기 어려운 내용도 일부 포함하였습니다.

이 글을 통해서 기본적인 쿠키의 개념들과 제가 겪었던 문제의 해결법을 같이 기술하겠습니다.

나만 빼고 다 알던 기본 개념

쿠키()란?

쿠키는 주로 브라우저에 저장되는 HTTP Protocol의 기록 정보입니다. 쿠키는 웹서비스를 이용할때 다양하게 사용이 되는데 쿠키를 제대로 이해하지 못하면 저처럼 multi-domain services을 사용할때 아주 골때리게 됩니다.

글을 처음 쓸때만 해도 디테일하게 설명하려고 했으나, MDN에 기본적인 내용이 잘 작성 되어있었기 때문에 이 글에서는 제가 겪었던 문제와 해결 방법에 대하여 기술하겠습니다.

🦸‍♂️ 용감한 쿠키는 오븐을 부수랬더니 내 머리를 부수고 있었다

1년전 코드에 feature를 추가해야할 일이 생겼다. 기존에 로컬 로그인 외에 oauth2 provider를 추가해야 했고 passport를 통해 서비스를 잘 붙였다. 삽질이 좀 있었지만 이정도면 준수한편 “잘 끝났군” 생각하며 로그아웃을 했는데 이게 왠걸 로그아웃이 안됐다. 새로고침을 해도 로그인 정보가 안사라졌다. 로그인이 되면 로그아웃이 되는건 너무 당연한데 왜 안됐을까?

이유는 토큰 정보를 쿠키에 저장하고 있는데 덮어쓰기가 안되고 있었다. 뭔가 이상해서 기존 로컬 로그인에 토큰 쿠키를 어떻게 세팅하는지 찾아봤다. 맙소사! 토큰을 발급하는 도메인에 로그인하고 응답을 받아 Body를 토큰으로 넣어주는걸 메인 도메인의 Next API에서 처리하고 있었다. 🤦‍♂️

🦊 구미호 쿠키는 꼬리가 세개만 보인다

이 서비스에서 사용하는 도메인은 6개다.(그래서 진정한 쿠키가 못됐나보다)(우리가 이겼다)

  • domain.com(프론트 메인 도메인)
  • token.domain.com(토큰 발급 도메인)
  • api.domain.com(API 도메인)
  • test.domain.com
  • token.test.domain.com
  • api.test.domain.com

(도메인 이름은 임의로 지어냈다)

이 중 토큰 쿠키를 보유하는 도메인은 domain.com이고 토큰을 발급하는 도메인은 token.domain.com이다. 그리고 api.domain.com은 토큰이 사용되는 도메인이다.

테스트 도메인의 경우는 테스트 도메인들끼리 통신하며 토큰 안에 aud claim이 test 스테이지를 포함하고 있다.

로컬 토큰이 발급되는 과정을 먼저 살펴보자면

  1. domain.com에서 사용자가 로그인 행위를 시작하면 브라우저에서 usernamepassword를 받습니다.
  2. 브라우저는 domain.com/api/login에 POST method로 username, password를 전송합니다.
    1. domain.com/api/login은 token.domain.comusernamepassword를 POST method로 전송합니다.
      1. token.domain.com에서 인증 처리가 완료되면 reponse로 token 정보들을 전송합니다.
    2. domain.com/api/login은 token 정보를 set-cookie로 응답을 전송합니다.
  3. 브라우저는 domain.com/api/login의 응답으로 token 정보를 쿠키에 저장합니다.

반면, 새로운 oauth2 로그인 토큰 발급 과정을 살펴보자면

oauth2.provider.com에는 이미 token.domain.com/oauth2/provider/callback이 등록 되어있습니다.

  1. domain.com에서 사용자가 로그인 행위를 시작하면 브라우저는 oauth2.provider.com으로 이동합니다.
  2. 브라우저는 oauth2.provider.com에서 로그인을 진행합니다.
  3. oauth2.provider.com이 인증을 완료하면 token.domain.com/oauth2/provider/callback으로 redirect합니다.
  4. token.domain.om/oauth2/callback oauth2.provider.com의 accesss_token, refresh_token을 받아 사용자를 인증합니다.
  5. token.domain.com/oauth2/callback 사용자 인증 후에 응답을 줍니다.
    1. set-cookie Header에 cookie domain attribute를 domain.com으로 설정합니다.
    2. 응답은 domain.com으로 redirect합니다.

🧟‍♂️ 좀비맛 쿠키는 왜 죽지 않을까?

위의 흐름에서 보면 domain.com에서 set-cookie를 한 token cookie는 따로 domain 설정을 안해줬기 때문에 domain.com으로 쿠키에 저장되었다. 그리고 token.domain.com에서 set-cookie를 한 token cookie는 domain attribute에 domain.com으로 도메인을 정해주었다.

이쯤에서 내 뇌피셜은 "음, domain.com 쿠키가 원래 사용되고 있었고, token.domain.com에서 domain.com 도메인으로 쿠키를 설정했으니 잘 되겠지? 흐흐흫" 이런 행복회로를 마구 돌리고 있었다.

그런데 이상하게도 여전히 로그아웃은 안되고 있었다. 나는 분명히 도메인을 설정해줬고 set-cookie domain.com으로 해줬는데 왜 안될까?

🧘‍♀️ 요가맛 쿠키는 요가로 마음의 수련을 하고 나는 CircleCI로 인내를 수련한다

몇가지 가정을 세웠다.

  1. 다른 도메인에서 설정해준 쿠키는 도메인별로 관리된다. 즉, 로그아웃 할때 쿠키를 삭제하는 것도 token.domain.com에서 해줘야한다.

  2. set-cookie attribute 값에 의해 사용되는 쿠키가 다르다.

    1. sameSite 설정에 의해 도메인별로 저장되는 쿠키가 다르다.

      예를 들어, sameSite가 Lax일때 사용되는 tokens라는 쿠키와 None일때 사용되는 tokens라는 쿠키가 다르다던가?

    2. secure 설정에 의해 도메인별로 저장되는 쿠키가 다르다.

      예를 들어, secure가 true인 tokens는 https로 바로 전송될 수 있고, false인 tokens는 한번 암호화 해서 저장한 쿠키라던가?

  3. express에서 cookie를 설정하는 다른 방법이 있다.

    1. set-cookie할때 overwrite 옵션이 있다.(cookies library)
    2. cookieClear를 해줘야한다.

문제는 도메인에서 set-cookie를 확인해야 하기 때문에 회당 10분이상 걸리는 CircleCI 배포를 테스트마다 돌려줘야 했다. 🤪 결국 위의 모든 방법(그 이상의 더 많은 방법)을 다 테스트 해봐도 결국 해답이 되는 것은 없었다.

😈 악마맛 쿠키는 악마로 변신한다

결론적으로, 이유는 브라우저 개발자도구로 확인해보니 패킷이 들어올때는 set-cookie에 domain attribute가 domain.com으로 들어오는데 저장될 때는 .domain.com으로 저장되고 있었다. 처음에는 위에서 얘기한 sameSite가 잘못 되었기 때문에 브라우저에서 .domain.com으로 저장되는 줄 알고 sameSite 설정을 건드렸다. 그러나 그건 해결방법이 아니었다. 급한 마음에 메인 도메인의 next api에 다시 작업을 할까 했지만 그러기엔 도메인 역할이 애매하고 작업 공수가 컸다.

온갖 삽질의 시도 끝에 결국 찾아낸 방법은 cookie를 저장하는 로컬 로그인과 로그아웃의 domain attribute를 .domain.com으로 설정해줬다.

🏃‍♂️ 쿠키런이 끝날 때는 결국 쿠키가 죽었을때 뿐이다.

뭔가 글을 쓰다 3주의 그 온갖 고생이 한 줄로 요약된 것 같아 현타가 온다 😞

지금 문득 생각해보니 왜 이렇게 간단한 방법을 왜 그렇게 한참을 헤메고 있었는지... 머리가 멍청하면 몸이 고생하는 것이었다. 이번 삽질을 통해 얻은 교훈을 정리하면 아래로 정리 할 수 있겠다.

  1. 아래의 가정 중 하나에는 Cookie domain attribute에 prefix로 dot(.)이 붙는 것 같다.(chrome, firefox 기준)
    • domain attribute를 설정해줬을 때
    • subdomain에서 domain attribute를 설정해줬을 때
  2. domain attribute가 다르면 동일한 name의 쿠키가 존재할 수 있다.

    domain.com으로 tokens cookie와 .domain.com으로 tokens cookie가 있다면 둘이 공존 할 수 있으며, 우선순위에 따라 브라우저에서 사용된다.

  3. 2와 같은 경우로 domain attribute가 다르면 cookie가 지워지지 않는다.(overwrite)
  4. 위의 이유로 혹시 여러 서브도메인을 갖추고 사용하는 경우, domain attribute를 상시 넣어주는 것이 좋다.
  5. domain attribute는 depth가 깊은 subdomain도 인식한다.

    .token.test.domain.com에서 발급한 .domain.com 토큰 쿠키를 .test.domain.com에서 사용할 수 있다.

  6. Chrome(@^80)의 경우 sameSite를 지정해주지 않았을 경우 sameSite=Lax가 기본 설정이다.(기존 sameSite=None)
  7. Firefox의 경우 expires attribute가 현재 시간 이전일 경우 무효된다는 콘솔 메시지가 있으나 Set-Cookie 헤더 자체는 적용되었다가 expires에 따라 즉각 삭제되는 것으로 보인다.
  8. Firefox의 경우 sameSite attribute(sameStie=None)가 secure attribute(secure=false) 설정 없이 사용되면 아래의 콘솔 warning을 나타낸다. 곧 유효하지 않도록 조치한다는 의미로 예상된다.

    쿠키 “tokens”는 “SameSite” 속성이 “secure” 속성이 없이 “None”이나 유효하지 않은 값으로 설정되어 있기 때문에 곧 거부됩니다. “SameSite” 속성에 대한 자세한 내용은 https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite를 읽으세요.

🧙‍♀️ 결론

  • 마녀 다양한 쿠키를 만들고 능력도 줬는데 쿠키들이 제멋대로 탈출한게 아닐까 싶다.
  • 쿠키들은 당분간 오븐에 아직 갇혀있을 필요가 있다.
  • 쿠키들의 탈출을 도왔던 내 흑역사가 결국 탈출하지 못한채 끝나서 다행이다.
  • 데브시스터즈는 이런 배경 속에 끝나지 않는 맵을 만들었을지 모른다.
  • 쿠키는 잘 쓰면 굉장히 유용하고 보안적인 조치도 많이 되어있다. 하지만 제대로 모르고 사용한다면 오븐이 아니라 내 멘탈이 바사삭 할 수도 있다.
  • 쿠키는 오랜 시간동안 사용되면서 조금씩 보안적 기술이 덧붙여지면서 발전해왔다. 오래된 기술이라고 해서 얕보면 큰코 다칠뿐더러 과거 지식을 가지고 사용했다간 서비스가 망가지기 십상이다.
  • 혹여 내용 중 수정 할 사항(PR) 있거나 질문이(Issue) 있으시면 링크 참고 부탁드립니다.