JWT + HttpOnly Cookie

date
Jan 8, 2024
slug
jwt-with-cookie
author
status
Public
tags
Etc
summary
type
Post
thumbnail
스크린샷 2024-04-07 오후 10.49.52.png
category
updatedAt
Apr 7, 2024 02:31 PM
사이드 프로젝트의 로그인 기능 구현 중에 고민한 내용을 공유해보고자 합니다.
현재 Next.js로 클라이언트를 구현하고 있어서, Client Rendering 이전에 인증/인가 정보가 필요할 수 있으므로 localStorage가 아닌 Cookie를 JWT 보관 방법으로 채택했습니다.
다만, Cookie는 localStorage와 달리 CSRF 위험이 있기에 이 점을 인지하면서 더 안전한 처리에 대해 고민해볼 필요가 있겠다는 생각이 들었습니다. 레퍼런스를 찾던 중에 HttpOnly를 사용해 로그인 기능 / 토큰 유효성 검증에 대한 처리를 하는 방법을 발견했습니다.
이것은 백엔드와의 긴밀한 협업이 필요한 부분이어서 (= 백엔드에서 해줄 일이 많다 ^,<) 이 방식대로 구현할 경우 얻는 이점과 어떤 방식으로 구현해야할지 문서화 + 플로우 차트를 그려 팀원분께 공유드렸습니다.
 
HttpOnly Cookie + JWT 방식으로 구현하게 된 이유에 대해 간략히 설명해보겠습니다.
우선, Cookie는 localStorage와 달리 CSRF 위험이 있다고 했습니다. 그럼 로컬 스토리지는 보안 위협 없는 안전한 방법일까요? 그렇지는 않습니다. 로컬 스토리지도 JavaScript로 직접 접근이 가능하기 때문이죠.
 
XSS 에 대해 들어보신 적 있으실 겁니다. 저는 컴퓨터 보안 수업때 들었던 기억이 나는 것(?) 같기도 합니다.

XSS (Cross-Site Scripting)

  • 악성 사용자가(해커) 웹 페이지에 악의적인 스크립트를 삽입하여 사용자의 브라우저가 그 스크립트를 실행하게 만드는 것입니다. 이 스크립트는 로컬 스토리지에 접근하여 데이터를 추출할 수 있습니다.
  • Cookie, localStorage 둘 다 취약합니다.

CSRF (Cross-Site Request Forgery)

  • 사용자의 브라우저가 사용자가 의도하지 않은 특정 행위를 서버에 요청하게 만드는 것입니다. 쿠키는 자동으로 모든 요청과 함께 브라우저에 의해 전송된다는 점을 이용합니다. 사용자의 브라우저를 통해 사용자가 이미 로그인해 있는 사이트에 요청을 보낸다면, 이 요청에는 사용자의 인증 쿠키가 자동으로 포함되어 전송되기에 CSRF공격으로 이어질 수 있습니다. (사용자의 인증된 상태를 이용해 의도하지 않은 작업을 수행하게 함)
  • 예시로는, 2008년 옥션 개인정보 유출 사건이 있습니다.
    • 공격 과정이 궁금하다면?
      해당 공격은 다음과 같이 진행 되었습니다. 1. 공격자(해커)가 img 태그에 코드를 추가하여 옥션 관리자한테 메일을 전송합니다. 2. 옥션 관리자는 옥션에 관리자 계정으로 로그인 되어 있던 상황입니다. 3. 옥션 관리자는 해당 메일을 확인합니다. 4. 관리자가 메일을 열면 해당 코드는 이미지를 받아오기 위해 해당 링크(url)로 이동} 5. 이미지는 크기가 0이기 때문에 확인 할 수 없습니다. 6. url의 내용처럼 id와 password를 admin으로 변경합니다.
      notion image
  • Cookie를 사용할 경우 취약합니다. 다만, CSRF 토큰 사용, SameSite 쿠키 속성 설정, 쿠키에 HttpOnly 플래그 설정 등을 통해 방어할 수 있습니다. HttpOnly플래그를 사용하면 JavaScript를 통한 직접적인 접근을 차단할 수 있어 XSS 공격으로부터 상대적으로 안전합니다.
 

[틈새] localStorage와 Cookie 비교

  • localStorage는 브라우저가 자동으로 요청에 포함시키지 않기 때문에 CSRF 공격에는 덜 취약합니다.
  • localStorage는 HTTPOnly 플래그를 사용할 수 없어, JavaScript를 통한 접근으로부터 보호할 수 없습니다.
  • Cookie는 HTTPOnly 플래그를 설정하여, JavaScript를 통한 접근으로부터 보호할 수 있습니다. 이는 XSS 공격으로부터 일정 부분 보호할 수 있게 해줍니다.
  • Cookie는 특정 도메인과 경로에 대해 제한을 두어, 해당 범위 내에서만 사용됩니다. 이는 더 세밀한 접근 제어를 가능하게 합니다. (localStorage는 SOP정책을 따르기에 동일한 도메인에서만 사용이 가능하다는 점!)
  • Cookie는 크기에 제한이 있으며 (일반적으로 4KB), 브라우저마다 저장할 수 있는 쿠키의 수에도 제한이 있습니다.
 

localStorage와 Cookie 중 무엇을 사용할까? → 사용 상황에 따른다

서비스의 특성, 보안 요구사항, 개발 및 유지 관리의 편의성을 고려하여 선택합시다.
  • API 중심의 단일 페이지 애플리케이션 (SPA): 로컬 스토리지가 선호됩니다. SPA는 주로 클라이언트 측에서 렌더링되며, API 요청을 통해 데이터를 주고받기 때문에 로컬 스토리지의 접근성이 유리합니다.
  • 전통적인 웹 애플리케이션: 쿠키가 유리할 수 있습니다. 쿠키는 서버와 클라이언트 간의 상태를 유지하는 데 적합하며, 보안 설정을 통해 XSS 공격에 대한 저항력을 높일 수 있습니다.
 
 

로그인 프로세스 설계

notion image
  1. 클라이언트에서 이메일과 비밀번호를 포함해 로그인 요청을 보낸다.
  1. 서버는 로그인 정보 검증 후 토큰을 생성하고 전송한다. 이때, AccessToken은 클라이언트에 직접 전달하고, RefreshToken은 httpOnlySecure 설정을 하여 쿠키에 담아 클라이언트에게 전송한다. ⇒ (Set-Cookie)
  1. 클라이언트는 AccessToken을 쿠키에 저장한다. *Refresh Token은 Access Token과 다르게, httpOnly 속성을 갖고 있어 JavaScript로 접근이 불가하다.
  1. 로그인 완료 (이후 프론트에서는 회원 정보 조회 등의 요청을 이어서 진행한다.)
  1. 토큰 유효기간 정책에 따라 AccessToken 유지
 

서비스에 다시 들어온 경우 (로그인 유지)

  • AccessToken이 있다 → 만료 기한 검증하자! (이걸 서버에서 할지 프론트에서 할지 논의 필요, 아니면 양쪽 다 validation하고 대부분 프론트에서 유효성 검사를 거쳐 핸들링 되도록 구현한다.) → 프론트, 백 모두 validation 구현
    • notion image
    • 만료 기한이 아직 안 지났다 → header에 AccessToken넣어서 서버로 요청 ⇒ 로그인
    • 만료된 토큰이다 → /refresh (임의의 endpoint) 로 요청
      • 서버는 HTTP 요청의 쿠키에서 RefreshToken을 추출 및 유효성 검사를 한다. (클라이언트가 서버에 요청을 보낼 때 HTTP 요청의 쿠키에 httpOnly RefreshToken이 포함되어 전송된다.)
        • 유효한 RefreshToken이 확인되면, 서버는 새로운 AccessToken을 생성하여 클라이언트에게 전달한다. 이때 RefreshToken만료시 401 내려준다.
          • 클라이언트는 응답 성공시 AccessToken을 저장한다. ⇒ 조용한 로그인
          • HTTP status code가 401이 내려오면 로그아웃 처리한다. ⇒ 이 경우는 RefreshToken이 만료되어 서버에서 401 상태 코드 받을 것이어서 클라이언트에서는 로그아웃 처리를 합니다.
 

토큰의 유효성 검증

  • Authorization이 필요한 API들을 사용할 때에 서버에서 AccessToken의 유효성을 확인하는 로직이 한 번 돌 것이다. (서버) 이때 AccessToken만료시 RefreshToken 존재 여부를 확인하는 로직을 태운 뒤 없다면 401, 있다면 재발급을 해준다.