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

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

 

 

개요

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

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

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

 

 

JWT 발급 시 주의사항

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

민감 정보 포함 금지

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 검증 시 주의사항

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

철저한 유효성 검사를 수행

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

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

  • 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 발급 및 검증 시스템을 구축하는 데에 조금이나마 도움이 되었길 바란다.