이상을 꿈꾸는 몽상가.. 프로그래밍을 좋아함..


JWT 에 대한 경험담

JWT Logo
(이미지 출처 : https://jwt.io)

JWT(JSON Web Token) 를 사용한 인증처리를 갖춘 시스템들을 구축해보면서 기억에 남았던 것들을 적어보겠습니다.
JWT에 대한 기본적인 설명들은 다른 곳에서도 쉽게 접할 수 있으니 생략하도록 하겠습니다.

이 글을 적게 된 이유는 SNS에서 반복해서 JWT의 실효성에 대한 의문을 갖는 글들을 자주 접하는데 SNS에 짧게 간단하게 답글을 남기는 것이 불가능해서 블로그 글로 작성해보려고 합니다.
근데 이런 시도를 3~4년 전에도 해봤었는데 이야기가 JWT에서 토큰 인증/인가로 흐르고 결국 모든 종류의 인증/인가에 대해서 흐르게 되서 범위가 커지고 글로 작성할 만큼 명확하게 알고 있지 못한 범위도 많아서 결국 그만뒀었던 것 같습니다.

이번에는 SNS 에서 자주 봤던 질문사항에 대해서 답변 하는 방식으로 간략하게 작성해볼 생각입니다.
근데 지금 적을 경험담들이 대부분 3~4년 전의 일이라 정확하지 기억하지 못할 수 있을 것 같아요. 최대한 정확하게 적으려고 노력해보겠으나 실수가 있을수 있으니 양해 바랍니다.

결론 예고

결론을 먼저 예고하자면,
SNS에서 가끔씩 이야기 되는 JWT는 실효성이 없다 라는 이야기는 틀렸다고 생각합니다. 어떻게 사용하느냐에 따라서 충분히 보안성을 만족시키면서 stateless 한 서버를 관리할 수 있게 도와줍니다.

JWT는 실효성이 없다 이야기들에 대한 반론을 이야기하며 JWT를 이용해서 충분히 일반 웹서비스를 구성 가능하다는 것을 이야기할 것입니다.
하지만 가능함에도 불구하고 불필요한 복잡도를 늘리기 때문에 단순한 일반 웹페이지를 제공하는 웹서비스에서는 다른 방법을 이용하는 것을 권장한다는 이야기를 할 예정입니다.

단순한 웹서비스가 아닌 아래와 같은 여러가지 상황에서는 JWT가 충분히 좋은 답안이 된다고 생각합니다.

  • 웹이 아닌 앱을 위한 서비스
  • 웹이지만 브라우저에서 단일 게이트웨이 통하지 않고 직접 다수의 resource 서버에 접근하는 경우
  • 브라우저에서 3rd party의 API 를 사용해야하는 경우 (반대의 경우도)
  • 3rd party 서버와 서버간 연동이 있을때 토큰 인증 방식으로 구성하는 경우
  • 웹도 포함하지만 위의 케이스와 복합적인 경우 (예 : 웹 & 앱 )

오해

Stateful JWT

SNS 에서 아래와 같은 오해들을 몇번 마주쳤던 것 같습니다.

  • access 토큰을 redis에 보관한다면 결국 stateful 구성이 되는것 아닌가?
  • stateless를 위해 JWT 를 사용했는데 refresh 토큰을 stateful 하게 관리하면 결국 stateful 구성이 되는것 아닌가?

access 토큰은 인증/인가 시점에 acess 토큰의 변조여부를 체크하는 과정을 거쳐서 제어가 가능하기 때문에 redis 같은 보관소에 토큰을 보관할 필요가 없습니다.
마찬가지로 refresh 토큰은 access 토큰 재발급 시점에 변조여부를 체크하는 과정을 거쳐서 제어 가능하기 때문에 stateless 하게 사용 가능합니다.
(여기서 변조여부 체크는 header, payoad 조합을 단방향 암호화(예:sha512)하여 나온 결과가 signature와 일치하는지를 체크하는 JWT 의 일반적인 유효성 체크 내용입니다.)

하지만 프로젝트를 진행하다보면 여러가지 이유에 의해서 stateful 하게 구성하게 되는 경우가 생깁니다.

로그인 기간 연장 기능

Access token 은 생성시점으로부터 유효기간이 정해집니다. 유효기간은 토큰 payload 에 심어져있기 때문에 변경이 불가능합니다.
하지만 일반적인 웹페이지의 요구사항 중에는 API 호출과 같은 사용자의 액션이 있다면 그 이후 로그인 유지 시간이 연장되기를 희망합니다.

아마도 이를 처리 하기 위해서 대부분의 시스템들이 JWT 토큰을 stateful 하게 관리할 수 밖에 없다는 이야기가 나왔을 것으로 예상됩니다.

제가 구성했던 시스템의 경우에도 동일한 요구사항이 있었고
이를 해결하기 위해서 resource API 호출 중에 401 응답을 받게 되면 내부적으로 토큰 갱신 API를 호출해서 토큰 갱신 후 resource API 를 재 호출하도록하는 인터셉터를 추가해서 처리했었습니다.

로그아웃한 토큰 제어

제 경우에는 로그아웃한 토큰에 대한 제어 때문이었습니다.

Access 토큰의 유효기간이 1분으로 굉장히 짧음에도 불구하고 로그아웃한 토큰의 경우 접근을 막아야한다는 강경한 보안 요청 사항이 있었습니다.
그전까지는 stateless한 구성을 잘 지켜냈었는데 어쩔 수 없이 로그아웃 시점의 토큰의 고유값을 redis에 refresh 토큰의 유효기간 동안 보관하고 단일 진입지점인 API 게이트웨이에서 redis 체크를 통해 제어를 했습니다. 참고로 refresh 토큰도 동일한 고유값을 가지고 있도록 구성했기 때문에 refresh 토큰을 이용한 access toekn 재발급도 함께 제어 됐습니다.

로그아웃시 브라우저에서 access, refresh 토큰이 삭제 되도록 잘 만들어두고 토큰의 유효기간을 짧게 가져간다면 굳이 별도의 관리가 필요하지 않다록 개인적으로 생각하고 이때는 불필요한 stateful 구성을 어쩔수 없이 진행했었다고 개인적으로 생각합니다.

참고로 이 경우는 로그아웃이 빈번한 시스템이 아니였기 때문에 redis에 저장되는 건수도 많지 않았고
단일 진입점인 API 게이트웨이를 가지고 있었기 때문에 stateful 하게 구성이 변경되었어도 새로운 resource 서버 추가 등에 대해서 영향없이 처리가 가능했었습니다.

단일 진입지점 API 게이트웨이는 필수?

API 게이트웨이를 구성할지를 정하는 것은 인증/인가만이 아닌 여러가지 이유들을 통해 정해집니다.
API 게이트웨이가 없고 클라이언트에서 각 resoure 서버로 직접 API 호출을 한다고하더라도 JWT로 대응 가능합니다.

이를 위해 크게 두가지 방법이 있는데 첫번째 방법은 인증서버가 SPOF(Single Point of Failure)가 될수 있기 때문에 저는 두번째 방법을 선호합니다.

  • 모든 API 요청에 대해 resource 서버가 인증서버에 토큰 변조여부를 질의
  • resource 서버가 변조여부 체크를 직접 진행하며 이를 위해 대칭키를 인증서버에 질의 (대칭키는 계속 변경됨) - RSA 방식으로 공유하는 라이브러리도 있음

토큰 탈취

토큰은 탈취당하면 토큰 유효기간 동안 위험에 무방비로 노출됩니다.

토큰 탈취는 클라이언트에서의 자바스크립트를 통한 탈취와 서버에서의 탈취로 크게 두 종류가 있습니다.

클라이언트 탈취 : 브라우저 Cookkie VS Local Storage

  • 로컬스토리지가 쿠키보다 더 최신 기술인데 더 안전하지 않나요?

로컬 스토리지가 HTML5 이후 나온 기술이라서 그런지 로컬스토리지가 더 안전하다라는 뉘앙스의 글을 많이 봤습니다. 절대 아닙니다.

가장 핵심은 자바스크립트가 토큰에 가능하면 안된다는 점입니다. XSS와 같은 웹취약성 공격을 통해 공격자가 작성한 자바스크립트가 웹페이지에서 동작한다면 토큰이 탈취되는 것은 너무나도 쉽습니다.
물론 HTML에 hidden input으로 토큰을 심어놓는 행위도 당연히 안됩니다.

반드시 쿠키에 보관해야하며 서버에서 Set-Cookie 응답헤더 생성 시점에 HttpOnly, Secure 설정을 잘 해주셔야 합니다.

그러면 자바스크립트가 쿠키의 토큰 저장영역에 접근을 못하게 됩니다. 그리고 API 호출시에는 브라우저가 자동으로 쿠키정보를 HTTP 헤더에 심어주면서 서버는 전달 받게 됩니다.
결과적으로 토큰에 대한 보관과 사용에 대한 모든 것은 서버 작업이 됩니다.

서버 탈취

  • refresh 토큰도 매번 전송하는데 access 토큰과 분리된 이유는 무엇인가요?

Resoure 서버의 API 는 access 토큰이 만료되었다면 401 응답을 내려야합니다. Refresh 토큰을 이용해서 access 토큰을 새로 만들어주는 갱신 API 는 인증서버에서 별도로 구성해줘야하고 앱이나 서버 클라이언트의 경우는 갱신 API 호출시에만 refresh 토큰을 전달해야합니다.
클라이언트가 브라우저인 경우라면, 인증서버 개발자는 refresh 토큰에 대한 Set-Cookie 응답헤더 생성 시점에 domain, path, SameSite(필요시)를 인증서버의 갱신 API를 호출시에만 해당 쿠키정보가 전달되도록 설정해야합니다.

이유는 resource 서버에서 refresh 토큰을 탈취할 수 있기 때문입니다.
Access 토큰은 탈취 당하더라도 짧은 유효기간 동안 위험에 노출되지만 refresh 토큰은 유효기간이 상대적으로 길기 때문에 사용자의 ID/PSWD가 resource 서버 담당자에게 노출되는 것과 비슷한 위험도라고 볼수 있을 것 같습니다. Resource 서버를 100% 신뢰할수 있다면 모르겠지만 resource 서버 중에는 3rd party가 포함될 수도 있기 때문에 주의해야하는 사항입니다.

그리고 브라우저 클라이언트의 경우 access token도 resource 서버 API 들을 호출할때만 전달되도록 Set-Cookie 응답헤더 생성 시점에 domain, path, SameSite(필요시)를 잘 세팅해줘야 합니다.

결론

위에서 명확히 이야기하지 않았지만
클라이언트가 브라우저인 웹서비스를 구성시 JWT를 이용하는 것은 웹취약성 그리고 브라우저 쿠키에 대한 이해도가 높아야합니다.
이해도가 높다하더라도 진행해보면 HTTPS, 도메인 등에 영향을 받기 때문에 테스트 난이도가 높아지는 단점이 있고 로컬 테스트를 위해서는 예외처리도 들어가는 등 시스템이 불필요하게 복잡해지는 단점도 있습니다.
그리고 만약에 도메인이 여러개이거나 하는 등의 상황으로 CORS 문제가 겹치게 된다면 꽤나 다양한 사황에 대해서 고민을 해야합니다.
그 외에도 평소에 만나지 못했던 숨어있는 브라우저의 다양한 제약사항들을 만나게 됩니다. 예를들어, HTTP 헤더에 Authorization 가 포함되었을 때와 포함되지 않았을 때의 서버측 CORS 설정 해결법이 크게 다르다는 점 등등..

만약에 웹서비스를 구성 중이며 3rd party 서버 없이 내부 서버들과 브라우저 사이의 연동만 필요하다면 JWT 보다는 다른 방식을 권장 드립니다.

  • WEB BFF(Back-end For Front) 를 stateful 하게 구성 : WEB BFF 에서는 redis를 이용해 고정토큰-JWT 맵핑을 관리하고 브라우저와 BFF 사이는 고정토큰으로 처리하되 BFF 와 resource 서버 사이에는 JWT로 호출하여 resource 서버들은 stateless 하게 처리. 토큰 갱신 작업은 BFF에서 내부적으로 인증서버와 연계하여 진행하며 브라우저 관점에서는 세션방식과 동일하게 처리되는 것처럼 보임.
  • 세션방식 : WAS 클러스터링을 통해 resource 서버간 공유하지 않고 redis 같은 중앙서버에 JSESSION_ID 를 보관하도록 WAS(예: tomcat) 기능 추가 작업 필요. 앱 혹은 다른 서버에서의 연동은 어떻게 처리할지 별도 고민 필요함.

브라우저를 통한 호출이 아닌 경우에는 JWT 사용은 충분히 도움이 됩니다.

앱이나 다른 서버에서 호출시에는 매 호출시마다 고정된 대칭키 형태의 토큰값을 사용하는 것보다 유효기간을 가진 토큰을 사용하기 떄문에 보안성이 올라갑니다.
뿐만 아니라 stateless한 구성이 가능해지기 때문에 단일 진입지점 API 게이트웨이가 없는 구성이거나 resource 서버 중에 3rd party 서버가 포함되어있더라도 SPOF 없이 구성이 가능합니다.

단일 진입지점 API 게이트웨이가 없는 구성이거나 resource 서버 중에 3rd party 서버가 포함된 경우도 클라이언트가 브라우저일 경우에는 동일한 어려움을 갖게 되는데 이 또한 위에서 언급한 WEB BFF 방식을 잉용해서 단일 진입지점을 만들어줌으로써 해결 가능해집니다.

결론적으로
브라우저 클라이언트만 가진 단순한 웹페이지를 구성하는 중이라면 세션방식 을 통해서 구성하는 것이 나을 것 같고
그 외 혹은 복합적인 클라이언트를 가진 시스템을 구성 중이거나 그럴 가능성이 있다면 JWT 를 이용해서 resource 서버들은 stateless 구성을 진행하고 stateful WEB BFF 를 별도 구성하는 방식이 나을 것 같습니다.


Associated Posts

관련된 주제를 살펴볼 수 있도록 동일한 Tag를 가진 글들을 모아뒀습니다. 제목을 눌러주세요.

i