프로젝트에 JWT를 적용하며 고민했던 것들에 대해 정리합니다. 다양한 의견들 남겨주시면 감사하겠습니다.
Refresh Token에 대해 조금만 확인해보면 DB에서 관리하자는 의견이 있다. 카카오는 작년 7월부터 Refresh Token을 DB로 관리하기로 변경했다.
DB 관리 이유는 로그아웃 구현을 위해 혹은 토큰이 탈취되었을 때, 제한을 위해 블랙리스트 기법을 사용하기 위함으로 볼 수 있다.
하지만 카카오나 네이버, 구글처럼 공용 인증 API를 운영한다는게 아니라면 Security+JWT를 다룬 이전 포스팅에도 말했다시피 JWT를 사용하며 DB 접근은 최소화하자는게 필자 생각이다.
또한 Refresh Token의 보안을 강화해도 문제점이 Access Token이 탈취되었을 때, 만료시간까지는 제한할 수 없다는 점이다. ( 30분일지라도 )
두 가지 조건이 충족하는 해결방안에 대해 고민했다
조건에 부합하려면 Access Token 내에 클라이언트 고유 정보를 담아야했다.
처음에 생각했던 건 MAC Address다. 최초 토큰 발급 시, JWT PayLoad에 클라이언트 MAC Address를 암호화해서 부여한다. 그리고 클라이언트는 매요청시, JWT와 함께 현재 MAC Address를 함께 요청한다. 서버는 복호화한 JWT 내부 MAC Address와 현재 요청 온 MAC Address를 비교하여 같은 사용자가 맞는지 검증한다.
다만 MAC Address 노출에 대한 위험성과 개인정보 수집 관련을 생각해 단방향 해싱을 고려하던 도중 비슷한 생각을 가진 블로그를 찾았다.
access token, refresh token 그리고... jwt? 그냥 unique token
상위 블로그에서는 이를 보완해 uuid v1,v4,v5 사용에 대한 생각을 정리해놨다.
이를 참조하면 다음과 같은 절차가 된다.
- Login 요청
- 기존과 동일하게 JWT를 생성하되 UUID v4와 새로 생성한 Secret Key를 전달한다.
- UUID v4와 새로 생성한 Secret Key는 서버에도 저장한다.
- 클라이언트는 요청보낼 때, TimeStamp - UUID v4를 최초 전달받은 Secret Key로 암호화해 JWT 뒤 .ddd로 붙여 전달한다. 즉, JWT 포맷이 aaa.ddd.ccc.ddd가 되는 것이다.
- 서버는 Token을 복호화해 얻은 UserKey를 통해 db에서 Secret Key와 UUID v4를 취득한다.
- ddd를 Secret Key로 복호화한 후, UUID v4가 맞는지 확인한다
- Timestamp가 30초 이내인지 확인한다
암호화된 토큰을 하나 더 사용한다는 생각이지만 매 요청마다 DB에 접근해야한다는게 걸린다. 다만 보안이 우선시 되는 곳에서는 해당 방안을 고려하는건 좋아보인다. 해당 방안을 사용하면 Refresh Token없이 Access token만으로 보안을 높일 수 있을 것이다.
상위 방안을 조건에 맞게 고민하던 중 논문을 하나 찾았다.
2019년에 등재된 논문으로 내용은 다음과 같다.
만료기간이 남았더라도 인증을 무효화 시킬 수 있고, SSL(Secure Socket Layer) 보
안통신 환경을 요구하지 않으므로 보안서버 구축에 어려움이 있는 소규모 서비스에도 적합하다
매우 매력적인 목적이다. 논문에서는 IP와 UUID를 통해 DB접근없이 토큰이 탈취되었을 때, 즉시 무력화시킬 수 있는 방안에 대해 기술하고 있다. 이를 반영하자면 다음과 같다.
- 클라이언트가 최초 서버에 요청 시, 서버는 UUID v4를 생성해 Cookie로 삽입한다
- 로그인 요청 시, PayLoad의 Private Claims에 해당 ip와 UUID를 암호화해 삽입한다.
- 클라이언트 요청 시, User_Agent를 통해 운영체제와 브라우저를 식별해 모바일 여부를 확인한다
- IP 변동이 잦은 모바일일 경우 UUID를 통해 검증한다.
- 모바일이 아닐 경우 IP도 함께 검증한다.
UUID를 통해 모바일에서도 호환성을 발휘하고 IP를 통해 토큰이 탈취되었을 때, 즉시 무력화시킬 수 있다. 게다가 DB 접근도 필요하지 않다..!
현재 반영하고자 하는 프로젝트는 코딩 테스트 프로젝트로 시험 특성상 IP가 변경이 된다해서 재로그인이 발생하면 안되는 프로젝트다. 따라서 IP 인증절차는 제외하고 UUID 검증만 해도 좋을 것 같다.
UUID를 일종의 JSESSIONID와 비슷하게 사용하려는 것 같은데, 최초 접근 시 발행하여 클라이언트의 고유 식별값을 만든다는게 재밌다. 만일 토큰만 탈취된다하면 Cookie에 검증할 UUID값이 없어 접근이 제한될 것이다.
또한 Access token의 만료기간을 짧게 설정할 이유도 없어지겠다. Access token만으로 인증하여 토큰이 탈취되었을 때 사용을 막기 위해 만료기간을 짧게 설정하고 갱신을 위한 Refresh token이 필요했던 것인데, 위와 같은 방법은 갱신을 위한 Refresh token이 아닌, 2중 인증을 위한 일종의 key형식이니 Access token의 만료기간을 넉넉하게 줘도 괜찮겠다. 탈취되어도 접근할 수 없을 테니까.
Access token과 UUID token은 모두 Cookie로 저장하는 편이 좋을 것 같다. 하나는 local storage, 하나는 Cookie로 저장할바에는 모두 Cookie로 저장하되, Http Only와 SameSite=Strict로 설정하면 CSRF 공격에 대부분 방어가 되고 또 매번 요청 헤더에 토큰을 넣는 작업을 하지 않아 코드도 깔끔해지겠다.
SameSite=Strict을 지원하지 않는 브라우저를 위해 차라리 서버에 CORSFilter를 생성해 같은 혹은 허용된 도메인이 아니면 차단하는 필터를 두는 것도 하나의 방안이다.
CSRF를 막기 위한 방안은 여럿 있으니 두 토큰 모두 쿠키를 통해 관리할 것이고 웃긴 점은 또 장점은 둘 다 쿠키로 관리하게 되면 서버에서 제어가 가능해진다는 것이다. 만료시간까지 서버에서 제어하지 못한다는게 JWT의 고질적인 단점이었는데 이를 보완해줄 수 있을 것 같다.
정리하자면 다음과 같다.
- 클라이언트가 최초 서버에 요청 시, 서버는 UUID v4를 생성해 Cookie로 삽입한다.
- 로그인 요청 시, PayLoad의 Private Claims에 UUID를 AES-256 암호화해 삽입하여 생성한 JWT를 Cookie로 내려준다.
- 클라이언트 요청 시, JWT 서명 검증과 PayLoad 내 UUID를 복호화한 값과 쿠키의 UUID값을 검증한다.
- 변조 위협이 있을 시, Cookie를 제거한다
SSL 인증서까지 반영해주면 금상첨화
뭔가 명확한 해결 방안을 찾다가 끝내는게 아니라 찝찝한 기분을 지울 수 없다. 하지만 여러 방안에 대해 생각하고 찾아보면서 깨달은 점은 정답은 없고 상황에 맞춰 보완해나가야 한다는 것이다.
성능보다 보안이 우선시 되는 곳이라면, 매 요청마다 db에서 검증하는 방안으로 가야될 것이며, 1.1 MAC Address의 방안도 좋아보인다.
논문 도입부에도 나와있지만 보안서버 구축에 어려움이 있는 소규모 서비스에서는 1.2 IP방안이 적합할 것이다.
현재 필자가 진행하고 있는 프로젝트는 IP가 바뀌면 재로그인이 발생해서는 안되므로 IP 검증은 제외한 채, UUID 인증 방식만 취하기로 했다.
정답은 없다. 하지만 JWT 구현 방식에 대해 고민하고 생각을 나눌 수 있는 다양한 의견들을 볼 수 있어서 즐거웠고 정리된 내용을 토대로 다른 환경에서는 이 곳에서 필요한 요소를 통해 각자 다양하게 구현해나가면 될 것이다.
참고
이거 근데
ip는 탈취당했더라도 암호화되어있어서 스푸핑공격이 되지않아서 메리트가 있긴한데
uuid같은 경우에는 탈취당하면 암호화되어있든 말든 암호화되어있는 상태로 쿠키에 담아서 요청하면 서버에서도 복호화진행해서 맞는 uuid로 결론을 내릴텐데 이 경우에서는 잘못된거아닌가요?