Technology/Web

JWT 기반 인가에 대한 고찰

ikjo 2022. 6. 10. 21:32

왜 JWT 의 장점을 버렸나요?

코드스쿼드 마스터즈 코스 과정에서 숙소 예약 서비스 팀 프로젝트를 진행하면서 미션 요구사항을 지키기 위해 세션이 아닌 JWT 을 통한 인가 기능을 구현해보았다. 더 나아가 JWT 에 대해 학습하면서 자연스럽게 access token 과 refresh token 에 대해 알게 되었고 access token 이 만료될 때 refresh token 을 검증하여 access token 을 갱신해주는 처리까지 구현해보았다. 이번에 구현한 대략적인 JWT 기반의 인가 과정은 아래와 같다.

 

이번 미션 과제 시 구현한 JWT 이벤트 흐름도

 

나름대로 위풍당당하게 구현 뒤 리뷰어의 리뷰를 기다렸는데, 아니나 다를까 담당 리뷰어로부터 이에 대해 질문을 받았다. "JWT 의 장점인 '서버의 자원을 사용하지 않는다'를 포기하면서 refresh 토큰을 redis 에서 관리하는 형태를 구성한 이유는 무엇인가요?" 이 질문에 대한 답변을 생각하다보니 생각이 많아졌다. 시중에 공개된 정형화된 방식을 그대로 따랐을 뿐 왜 이렇게 해야하는지에 대한 고민이 부족했었던 것이다.

 

이에, 각각의 과정들의 필요성에 대해 고찰해가며 위와 같은 프로세스의 타당성을 검증하고자 한다.

 

 

JWT 의 장점이 뭔데요?

우선 JWT 의 장점은 리뷰어가 질문하신 것처럼 '서버의 자원을 사용하지 않는다.'라는 것이다. 즉, stateless 하다는 것인데, 이는 서버의 자원을 사용하는 세션의 단점과 대치되는 개념이다. 세션의 경우 stateful 하다. 만일 여러 WAS 가 있을 경우에는 WAS 간 세션 데이터를 공유하기 위해  세션 클러스터링을 하거나 별도의 storage 를 둔다. 하지만 이로 인해 서버의 부하 부담을 가중시킨다는 단점이 있다.

 

이러한 세션과 달리 JWT 은 서버에 자원을 두지 않는 차이가 있다. 이에 JWT 를 기반으로 한 인가 시스템은 세션 기반 인가 시스템 대비 세션 클러스터링이나 별도의 storage 를 둘 필요가 없어 서버의 부하 부담을 줄일 수 있다는 장점이 있다.

 

 

JWT 의 치명적인 단점과 보완

하지만 JWT 만 쓰다보니 문제가 생겼다. JWT 을 탈취 당하면 이를 무효화할 수 있는 방법이 전혀 없는 것이다. 왜냐하면 서버에서는 해당 JWT 에 대한 정보를 따로 저장하고있지 않기 때문이다. 단순히, 해당 JWT 가 유효한지 여부만 검증할 수 있을 뿐이다. 따라서, 탈취한 사람이 이를 악용하고자 마음을 먹으면 피해가 커지게 된다.

 

물론, 세션 기반의 인가라고 해서 JWT 기반의 인가 보다 탈취에 더 안전한 것은 아니지만, 서버 입장에서는 해당 세션이 공격자에 의해 탈취(세션 하이재킹)당했다는 것을 감지할 수만 있다면 서버에 저장된 세션 데이터를 무효화시킬 수 있다.

 

이를 개선하고자 JWT 을 access token 과 refresh token 으로 분리한 것이다. 이때, access token 은 실질적으로 인증 여부를 검증하는 용도로 사용되며 유효 시간을 매우 짧게 한다. 이에, access token 을 탈취 당하더라도 그 유효 시간이 매우 짧기 때문에 피해 파급을 줄일 수 있다.

 

다만, access token 의 유효 시간이 짧기 때문에 사용자는 인가 절차를 위해 빈번하게 로그인을 해야하는 번거로움이 생기는데, 앞서 언급했던 refresh token이 이를 해결해준다. 최초 사용자가 로그인을 하면 서버는 클라이언트에 access token 과 refresh token 을 응답해준다. 이후 클라이언트가 서버로 전송한 access token 이 만료되면 서버는 클라이언트에 access token 이 만료되었다는 응답을 보내고 클라이언트는 다시 서버에 refresh token 을 전송하여 access token 재발급을 요청한다.

 

서버는 이 refresh token 을 유효한 토큰인지 검증하여 클라이언트에 access token 을 재발급해준다. 만일 refresh token 이 유효하지않다면 클라이언트에 refresh token 이 유효하지 않다고 응답한다. 이처럼 refresh token 은 유효시간이 짧은 access token 을 보완하기 위해 나온 것으로 access token 보다는 유효 시간을 넉넉하게 잡는 것이 좋다. 참고로, refresh token 의 용도가 access token 과 다를 뿐이지 access token 이나 refresh token 이나 똑같은 JWT 일 뿐이다. 

 

 

Refresh Token 을 왜 서버에 저장하는가?

단순히, access token 을 재발급하는 것만 생각하면 굳이 refresh token 을 서버에 따로 저장할 필요가 없다. 하지만, 앞서 JWT 의 단점은 무효화 대책이 없다는 것이었다. 만일 refresh token 도 탈취된다면, refresh token 을 통해 계속해서 access token 을 재발급 받을 수 있으므로 JWT 단점은 해결되지 않는다.

 

결국, refresh token 을 서버에 저장하여 refresh token 을 탈취당했다는 것을 감지했을 때, 해당 refresh token 을 무효화시켜야 한다. 이때, redis 에 refresh token 을 저장한 이유는 여러 서버간 해당 데이터를 공유할 수 있고 In-Memory DB 라 RDBMS 보다는 I/O 성능이 좋기 때문이다. (memcached 도 대안이 될 수 있다.)

 

하지만, 여기서 의문이 들 수 있다. "세션의 stateful 특성으로 인한 문제점을 개선하고자 나온 것이 JWT 인데 탈취당하여 악용될 우려가 있어 결국에는 서버의 자원을 똑같이 사용하고 있는 것이 아닌가? 그렇다면 세션을 사용하지 굳이 JWT 를 사용할 이유가 있는가?"

 

이 의문의 답으로는 세션 기반 인가 보다는 적게 DB 에 접근한다는 점이 있다. 세션 기반 인가는 인가 때마다 DB 에 접근해야하지만, 앞선 프로세스를 적용한 JWT 기반 인가는 클라이언트가 access token 을 서버로 전송 시에는 DB 에 접근하지않고 refresh token 을 서버로 전송 시에만 DB 에 접근한다. refresh token 을 통한 access token 재발급은 상대적으로 적게 일어나기에 세션 기반 인가 보다는 서버 성능면에서 유리한 측면이 있다.

 

 

탈취당했다는 것을 어떻게 감지하는가?

더욱이, 앞서 계속해서 '탈취당했다는 것을 감지'라는 표현을 써왔는데, "서버 측에서는 JWT 나 세션이 탈취당했다는 것을 어떻게 감지하는가?" 라는 의문이 생길 수 있다.

 

많은 방법이 있겠지만 대표적인 방법으로 Rotation 기법이 있다. 이는 한 명의 사용자에게 하나의 토큰만 허용하고 이를 일회성으로 취급하면서 이미 사용된 토큰을 무효화 처리한 후 누군가 해당 토큰으로 다시 요청을 보냈을 경우 이를 누군가에 의해 해당 사용자의 토큰이 탈취당한 것으로 간주하는 것이다. 이때, 해당 사용자의 식별 정보로 발급된 모든 유효한 토큰 무효화 처리한다. 왜냐하면, 서버 입장에서는 무효한 토큰에 대한 재사용이 감지되었지만 현재 유효한 토큰을 정상적인 사용자가 지니고 있을지, 공격자가 지니고 있을지 정확하게 알 수 없기 때문이다.

 

앞서, refresh token 의 경우 redis 에 저장하고있었다. 즉, refresh token 은 애초에 stateful 했기에 refresh token 이 탈취당했다는 것을 감지하기 위해 Rotation 기법을 적용해볼 수 있다. 이를 Refresh Token Rotation 기법이라고 한다.

 

 

 

안전한 저장 및 교환 전략 수립

사실, 지금까지 JWT 나 세션을 탈취당했을 경우에 대한 대비책에만 초점을 뒀었으나, 근본적으로는 탈취당하지 않도록 하는 것이 중요하다.

 

대표적인 웹 공격으로는 CSRF(Cross Site Request Forgery) 와 XSS(Cross Site Scripting) 가 있는데, 이러한 공격에 대비하여 클라이언트 측에서는 해당 데이터를 보안상 어느 저장소에 저장할지 고민해야한다. 클라이언트 측의 데이터 저장 수단으로는 대표적으로 쿠키, session storage, local storage, 상태 관리 라이브러리가 있다.

 

일단, sesstion storage 와 local storage 는 cross site 에서 접근할 수 없으므로 CSRF 공격에는 안전하다. 다만, XSS 공격에는 취약하다는 단점이 있다. 하지만 target server 내 XSS Filter 가 있다면 XSS 공격에 대응할 수 있다.

 

또한, 쿠키는 CSRF 와 XSS 공격에 모두 취약하지만 SameSite 속성과 HttpOnly 속성을 통해 CSRF 와 XSS 공격에 대응할 수 있다.

 

마지막으로 상태 관리 라이브러리의 경우 CSRF 와 XSS 공격에 안전하지만 사용자의 새로고침 시 데이터가 소멸될 수 있다는 단점이 있다. 이때, access token 은 상태 관리 라이브러리에 저장하기 적합하다. 왜냐하면 새로고침해도 refresh token 으로 access token 을 재발급받을 수 있기 때문이다. 반면에, refresh token 은 어느정도 생명주기가 있는 만큼 상태 관리 라이브러리에 저장하기에는 부적합하다. 새로고침할 경우소멸되어 사용자는 다시 인증해야하기 때문이다.

 

더욱이, 클라이언트와 서버간 데이터를 주고받음에 있어 네트워크 상에서 발생할 수 있는 공격에 대비한 클라이언트와 서버간 통신 시 AES, RSA 등 암호화 전략도 수립해야한다.

 

 

여전한 JWT 에 대한 의문점과 보완책

여전히 의문이 있다. "access token 의 유효 시간이 짧더라도 어찌됐든 탈취당하면 그 짧은 시간에라도 피해를 볼 수 있는 것이 아닌가?" 즉, 현재로서는 access token 을 당취 당했을 때 잔여 유효 시간 동안 피해 파급을 막을 대책이 없다. 이는 현재 JWT 기반 인가 시스템의 취약점이다. 다만, access token 의 유효 시간을 얼마나 적게 설정하냐에 따라 피해 파급 정도는 달라지리라 생각한다.

 

사실, 세션을 사용한다고 해도 세션 데이터를 탈취당하고 탈취당한 것을 감지해낼 때까지는(서버에서 해당 세션을 무효화할 때까지는) 해당 세션을 통한 악의적인 요청이 허용된다. 보안을 강화하기 위해 성능을 희생할 수 있다면 앞서 언급했던 Rotation 기법을 적용하여 한 명의 사용자에게 하나의 토큰만 허용하고 이를 일회성으로 취급한다면 탈취로 인한 피해 파급을 현격하게 줄일 수 있을 것이다. 당연히, 이 경우 더이상 stateless 한 장점이 없어지게 된다.

 

 

보안과 성능은 트레이드 오프

리뷰어의 질문으로 막연히 구현했었던 JWT 기반의 인가 시스템에 대해 깊게 고민해볼 수 있었다. 이러한 고민을 통해 JWT 나 세션이나 취약점이 있을 수 있으며, 이를 보완하는 과정은 보안과 성능 간 트레이드 오프 관계에 있다는 사실을 배울 수 있었다. 보안을 생각한다면 검증 과정이 추가되고 이는 곧 성능 저하로 이어지며, 성능을 생각하면 검증 과정이 간소화되고 이는 잠재적으로 취약점이 될 수 있는 것이다.

 

개인적으로 보안을 생각한다면 stateless 한 JWT 보다는 stateful 한 세션을 도입함이 유리한 점이 많지 않을까 싶다. 앞서 언급했듯이 stateless 한 access token 의 경우 유효시간이 짧더라도 탈취 시 무효화할 대책이 없기 때문이다. 즉, stateless 라는 서버 성능 상 이점이 보안의 허점이기도 한 것이다.

 

다만, 클라이언트 측에서 access token 을 안전하게 저장하고 서버와 access token 을 주고받을 때에도 암호화를 철저히 한다면 access token 을 탈취당할 가능성을 낮출 수는 있겠지만, 최악의 상황을 가정했을 때에는 여전히 불안 요소이기도 한 것이다.