JWT를 안전하게 발급하고 검증하는 방법

본 글은 JWT의 정의를 알고 있고, JWT를 사용해본 적이 있는 사람들을 대상으로 작성되었습니다.

 

 

개요

스크린샷 2024-11-23 162900.png

최근에는 개인이든 기업이든 사용자 인증을 위해 JWT를 사용하는 경우가 많다. JWT는 사용자 인증을 수행하는 아주 간단한 방법이며, 우리에게 친숙한 JSON이라는 형태로 인증 정보를 다룰 수 있어 아주 좋은 기법이다.

그러나 JWT의 보안 취약점에 잘 대응하지 못하면, 사용자의 정보를 탈취당하거나 제3자가 사용자인 척 행동하는 것에 속수무책으로 당할 수 있다. 이러한 사고를 예방하기 위해서는 JWT를 안전하게 발급하고 검증하는 방법들을 숙지하는 것이 중요하다.

따라서, 본 글에서는 JWT를 발급하고 검증하는 주체의 시각으로 JWT를 바라보며 JWT를 안전하게 다룰 수 있는 방법들을 소개해보고자 한다.

 

 

JWT 발급 시 주의사항

Designer (4).jpeg
열차 티켓을 제작하는 사람을 상상해보자. 어떻게 하면 아무나 쉽게 따라할 수 없는 티켓을 만들 수 있을까?

민감 정보 포함 금지

JWT는 payload에 사용자 정보를 포함시킬 수 있다는 특징을 갖는다. 그러나, JWT의 payload는 단순히 base64로 인코딩된 것이기 때문에, 토큰을 획득한 사람은 누구든 토큰 내부의 데이터를 확인할 수 있다.

따라서, 민감 정보는 토큰에 포함하는 대신 서버의 DB와 같은 안전한 저장소에 보관하고, 이를 조회할 수 있는 별도 값(ex: PK)을 포함하는 편이 좋다.

만료 시간을 반드시 설정

발급한 JWT의 유효기간을 무제한으로 설정하면 Refresh Token과 같은 복잡한 기법을 사용할 필요가 없는데, 왜 만료 시간을 설정하는 것일까?

그 이유는, JWT를 획득한 사람이 누구든 이 토큰을 통해 유효한 인증 절차를 거칠 수 있기 때문이다. 토큰이 탈취당했을 때 이 토큰에 만료 시간이 없다면 탈취자는 영영 해당 사용자의 흉내를 낼 수 있게 된다.

💡 Refresh Token이 탈취당해도 같은 위험이 생기는 게 아닌가?
맞다. 그렇기 때문에 Refresh Token에도 만료 시간을 설정하는 것이 좋다. Refresh Token의 재발급 API도 만들고, 별도의 시크릿 정보(ex: 사용자의 아이디/비밀번호)로 검증하여 토큰을 발급해주는 것이 좋다.

안전한 서명 알고리즘을 사용

악의적인 제3자가 발급자와 완전히 동일한 방식으로 토큰을 발급할 수 있게 된다면, 그래서 검증자가 제3자가 발급한 토큰을 통과시켜준다면 인증은 완전히 무의미해진다. 이를 예방하기 위해서는 토큰을 발급하는 과정에서 사용하는 암호화 알고리즘이 어려울 수록 좋다.

주로 암호화에는 대칭키를 사용한 HS256, 비대칭키(공개키-개인키)를 사용한 RS256 방식이 많이 사용된다.

대칭키를 사용하면 속도가 더 빠르다는 장점이 있지만 Key를 탈취당했을 때의 위험성이 더 크며, 비대칭키의 경우 개인키로 토큰을 암호화하고 공개키로 토큰을 검증하여 공개키가 탈취당해도 발급자의 개인키만 잘 보호하면 안전하다는 장점이 있으나 대칭키에 비해 속도가 느리고 더 복잡하다는 단점이 있다.

 

 

JWT 검증 시 주의사항

Designer.jpeg
이번에는 열차 티켓을 검사하는 역무원을 상상해보자. 어떤 검증을 통해 티켓이 진짜인지 판단할 수 있을까?

철저한 유효성 검사를 수행

토큰의 검증자는 언제든 누군가 발급자를 흉내낸 토큰을 발급해 가져올 수 있음에 유의해야 한다. 검증자에게는 이 가짜 토큰들을 잘 판별해낼 책임이 있다.

검증자는 아래의 항목들을 반드시 검증해야 한다.

  • Signature 검증 - 알맞은 키로 서명되었는지 검증
  • 유효시간 검증 - 이미 만료된 토큰인지 검증
    • 유효시간 검증 단계에서 실패한 경우 “토큰이 만료되었음”을 알 수 있는 응답을 제공하는 것도 중요하다. 공격자가 아니라 단순히 토큰의 갱신을 놓친 정상 사용자라면 만료 사실을 인지하고 다시 토큰을 발급받을 수 있어야 하기 때문이다.

그 외에도 발급자와의 합의를 통해 다양한 검증 기준을 만들 수 있다. payload 내에 발급자 정보(iss)를 포함시키거나, 같은 토큰을 단 한 번만 사용할 수 있도록 제한하기 위해 토큰에 고유 아이디(jti)를 부여하는 것이 이에 해당한다.

철저한 로깅 및 모니터링 수행

토큰 검증 요청이 들어왔을 때 로그 등을 통해 이 토큰 정보들을 잘 수집하는 것이 좋다. 장애를 발견하고 추적하는 데에도 도움을 주지만, 잘못된 토큰이 들어왔을 때 공격자가 어떤 방식으로 공격을 시도했는지 파악하는 데에도 큰 도움이 된다.

 

 

보안 강화를 위한 추가 고려 사항

그 밖에도 JWT를 사용할 때 보안을 강화할 수 있는 다양한 기법들이 존재한다. 이 기법들에 대해서도 살펴보자.

HTTPS를 사용

JWT 토큰은 보통 HTTP 헤더에 담겨 운반된다. HTTP도 평문이고 JWT도 평문이므로 JWT 토큰을 포함한 HTTP 요청/응답 패킷을 획득한 사람은 누구든 이 JWT 토큰을 획득하고 내부 정보를 확인할 수 있게 된다.

HTTPS를 사용하게 되면 HTTP 패킷 전체가 암호화되므로, 패킷을 가로채 헤더 내 정보를 보려는 중간자 공격(MITM)을 막을 수 있다.

토큰의 저장 위치를 적절히 선정

이것은 발급자/검증자의 입장이라기보다는 토큰을 발급받고 인증을 요청하는 클라이언트(ex: 브라우저) 입장에서 고려하면 좋을 사항이다.

클라이언트 입장에서 JWT 토큰을 소유할 때 조심해야 할 공격으로는 크게 XSS와 CSRF 2가지가 있다. 토큰을 쿠키에 저장하게 되면 항상 HTTP 요청에 포함되므로 CSRF에 취약하고, 토큰을 로컬/세션 스토리지에 저장하게 되면 스크립트를 통해 토큰을 획득할 수 있어 XSS 공격에 취약하게 된다.

이 중 어디에 토큰을 저장하면 좋은가에 대한 명확한 정답은 없다. 하지만 이러한 위험성을 잘 숙지하고, 어떤 공격의 방어에 더 초점을 맞출지 애플리케이션의 특성을 잘 고려하여 선정하는 것이 중요하다.

보안적 측면에서는 액세스 토큰을 메모리에 저장하고 리프레시 토큰을 쿠키에 저장한 뒤 새로고침할 때마다 리프레시 토큰을 통해 액세스 토큰을 재발급 받는 방법이 가장 안전하다고 보이지만, 이 또한 구현 복잡도가 올라가고 토큰 발급이 너무 자주 일어날 수 있다는 단점이 존재한다.

키를 자주 교체

토큰의 암호화에 사용하는 키가 탈취당할 위험을 완전히 배제할 수는 없다. 따라서, 키를 자주 교체하는 것이 좋다.

하지만 키를 교체할 때 한 가지 주의해야 할 점은, 기존에 발급된 토큰들 중 만료시간이 남아 있는 토큰들도 검증을 통과할 수 있도록 해야 한다는 점이다. 이는 키를 교체한다고 해서 기존에 사용하던 키를 바로 제거하면 안 되는 이유이기도 하다.

기존 토큰을 검증하기 위해, 기존 토큰들이 유효한 일정 기간동안은 기존 키와 새로운 키를 모두 유지하는 것이 좋다. 이렇게 되면 검증에 사용되는 키는 다수 개가 공존할 수 있게 되는데, 이 때 어떤 키를 사용해 검증할지를 효율적으로 결정하기 위해 jwt 헤더에 kid라는 값을 포함하면 좋다.

jwt의 kid란, 발급할 때 검증에 사용할 공개키의 식별값을 담는 곳으로, 검증자는 kid를 통해 검증에 사용할 공개키를 바로 결정할 수 있다. kid가 없다면 검증 키 N개에 대해 모든 키를 순회하며 O(N)의 시간복잡도로 검증을 수행해야 하지만, kid를 이용하면 바로 검증 키를 획득해 O(1)의 시간복잡도로 검증을 수행할 수 있게 된다.

 

 

마치며

지금까지 JWT를 발급하고 검증할 때 주의할 점들, JWT의 보안을 강화하기 위한 추가 고려 사항들을 살펴보았다.

사용자의 정보와 권한을 소중히 다루는 것은 서비스 제공자가 사용자에게 보장해야 할 기본적인 신뢰의 요소 중 하나이다. 이 글이 안전한 JWT 발급 및 검증 시스템을 구축하는 데에 조금이나마 도움이 되었길 바란다.