앞선 JWT 토큰 탈취에 이어 프로젝트를 진행하던 중 코치님께서 다음과 같은 질문을 해주셨다.
'리프레쉬 토큰을 구현하지 않고 소셜 로그인을 먼저 구현하신 이유가 있을까요?'
이 질문을 받았을 때, 먼저 들었던 생각은 리프레쉬 토큰이 뭐지? 였다.
당연히 바로 구글링을 했고 그 결과, 리프레쉬 토큰은 jwt의 stateless한 방식 때문에 생기는 보안적인 문제점을 보완하기 위한 방법이였다 ! 분명 저번 글에서는 단순히 JWT가 탈취당하면 방법이 없다. 라고 끝냈었는데 방법이 더 있었다..
궁금증이 생겨 여러가지 관련 글들을 읽어봤고 내가 이해한대로 리프레쉬 토큰 방식의 과정을 정리하면서 글로 써봤다. (아래는 팀원들한테 공유한 내용이다.)
코드 리뷰 중에 코치님이 이런 리뷰를 남겨주셨는데 저번에 JWT 토큰이 탈취당했을 때, 털리면 그냥 방법이 없구나.. 생각하고 더 찾아보지 않아서 솔직히 refresh token이란게 있는지 몰랐습니다. 다들 아시면 좋은 정보일 것 같아서 제가 공부한 내용을 공유드립니다!
지금 저희 프로젝트의 토큰은 만료 기간이 존재하지 않기 때문에 보안 상으로 많이 위험합니다. 만료 기간이 존재하지 않는다면 토큰이 탈취 당했을 때 공격자는 영원히 해당 유저처럼 위장할 수 있게 됩니다. JWT는 Stateless(서버에 저장되지 않음.)한 방식이기 때문에 서버 측에서는 해당 토큰을 가진 유저가 정말 본인인지 파악할 수 없을뿐더러 탈취 됐다는 사실 조차도 파악하기 힘듭니다. 그래서 토큰에는 만료 기간이 있는 것이 일반적이고, 토큰이 만료됐을 경우에는 로그인 절차에서 다시 사용자에게 토큰을 발급해줍니다.
그런데 여기서 만료 기간을 과연 얼마나 길게, 혹은 짧게 설정해야 되냐는 문제가 생깁니다. 기간이 너무 길다면 공격자가 토큰을 탈취 했을 때, 만료 기간이 끝나기 전까지 해당 사용자인 척 위장할 수 있고 만료 기간이 너무 짧으면 사용자가 번거롭게 로그인을 자주 해야 될 수 있습니다.
그렇기 때문에 로그인 시 JWT 토큰을 2개 발급하여 access token, refresh token이라는 이름으로 분리합니다. 뒤부터는 줄여서 access, refresh라고 부르겠습니다. access는 기존의 토큰처럼 사용자의 id와 role 정보를 담고 있고, refresh는 사용자 인증의 용도가 아닌 새로운 access token을 생성하는 용도로 사용됩니다. 2개의 토큰은 모두 클라이언트 측의 세션에 저장하고, refresh만 또 따로 서버에 저장합니다.
access의 유효 기간은 짧게(2시간), refresh의 유효기간은 길게(2주)로 설정합니다. 클라이언트 측은 유저 검증이 필요한 API를 사용할 시, access와 refresh를 서버에 보내고 만료 됐을 시 새로운 access를 발급 받습니다. 만약 공격자가 access를 탈취했더라도 짧은 유효 기간이 지나면 해당 토큰을 사용할 수 없게 됩니다. 유효 기간 동안의 피해는 막을 수는 없지만 최소화하기 위함입니다. 여기서 access의 유효 기간은 2시간이기 때문에, 만료가 끝나면 앞서 말씀드렸던
토큰이 만료됐을 때 로그인을 다시 해야된다.
라는 문제가 생기는데, 이것을 refresh를 통해 해결합니다. access가 만료 됐을 때, 남아있는 refresh를 서버에 보내 DB에 있는 refresh 정보와 일치하면 새로운 access를 서버에서 발급해줍니다. refresh가 없는 공격자는 access를 새로 발급할 수 없습니다.그럼 여기서 refresh가 탈취 당하면 유효 기간 동안 access를 마음대로 생성할 수 있지 않나? 라는 의문이 생기실 수 있는데, 맞습니다. 하지만 refresh에서 access를 발급하는 과정은 서버에서 관리하기 때문에 사용자의 IP를 검증해서 기존과 다르다거나, 신고된 아이디의 refresh일 경우에 서버에서 추가적인 검증을 할 수 있는 여부가 있습니다. 아마 네이버나, 구글, 페이스북 등의 웹사이트에서 기존과 다른 곳에서 로그인했을 때 경고 메세지가 왔던걸 보신 적이 있으실겁니다. 그래서 해당 경우에는 refresh 자체를 폐기시켜 access를 발급하지 못하게 합니다. 쓰다보니 너무 길어졌는데, 결론은 기존의 jwt 방식은 stateless라는 특징 때문에 보안에 굉장히 취약하다. 때문에 refresh 토큰을 이용하면 추가적인 방어 절차를 거칠 수 있다. 입니다!
틀린 내용이 있을 수도 있습니다!
질문을 받은 날, 글로 정리해보면서 어떻게 구현해야 될 지 프로젝트에 대입해서 생각해봤다.
근데 역시 이론과 실전은 다르다고.. 구현하면서 많은 문제점을 만났고, 생각해야 될 부분도 많았다.
특히 기존에 세션 스토리지에 있던 토큰에 대해 왜 세션일까?라는 생각을 못해봤는데, 로컬 및 세션 스토리지와 쿠키의 차이점을 명확하게 알 수 있었던 계기가 되었다. 이 부분에 대해서는 다음 글에서 더 자세히 작성하겠다 !