안녕하세요? 이번 프로젝트르 하며 로그인을 구현하게 되었습니다. 처음 해보는 터라 공부를 하며 해보았는데요.. 그 과정에 대해 글을 작성해보려 합니다!
🔑 문제 상황
로그인을 구현하려면 다음과 같이 진행이 됩니다.
👨🏻💻 사용자가 로그인 요청을 보낸다.
🗄️ 서버는 사용자의 정보를 확인하고 로그인 성공 응답을 보내준다.
간단합니다. 그러나 문제는 HTTP는 스테이트리스(stateless) 프로토콜이라는 것입니다. 이게 무슨 뜻이냐 하면 이미 지나간 요청과 응답의 상태를 기억하고 있지 않다는 뜻입니다. 가령 위의 과정을 거쳐 로그인을 성공해도 다른 페이지로 넘어가면 로그인을 했었는지 기억을 못하고 다시 로그인 요구를 한다는 것입니다.
페이지를 넘기거나, 새로고침을 하면 계속 님 누구냐고 물어본다..? 유저 이탈률 100%를 찍을 듯 합니다.. 이를 해결하기 위해 등장한 것이 jwt 토큰인데요!
🔑 jwt란 무엇인가?
jwt는 JSON Web Token의 약자로 RFC 규격에 의해 정의되어 있는데요! 바로 JSON 객체에 인증에 필요한 정보를 담아서 비밀키로 서명한 토큰을 생성하는 기술입니다. jwt를 이용한 로그인 과정이 어떻냐하면 이렇습니다.
👨🏻💻 사용자가 로그인 요청을 보낸다.
🗄️ 서버는 secret key를 사용해 jwt token을 생성하고 이를 헤더에 담아 클라이언트에게 보낸다.
위의 과정과 무엇이 다르냐하면 바로 jwt token을 발급해주는 곳이 다릅니다. 이를 이해하기 위해서는 아래의 약간의 선수 지식이 필요한데요.. 잠깐 짚고 넘어가겠습니다.
🤔 세션 기반 인증 vs 토큰 기반 인증
둘의 용도는 같습니다. 바로 사용자가 계속해서 로그인을 하며 본인을 나 유효한 사용자야ㅠㅠ!! 하고 서버에게 증명하기 않도록 도와준다는 점입니다.
세션 기반 인증
세션이란 브라우저와의 통신이 끝날때까지 사용자의 요청을 모두 하나의 상태로 보고 상태를 유지시키는 기술입니다. 세션 기반 인증을 그림으로 나타내면 다음과 같습니다.
👨🏻💻 사용자가 로그인 요청을 보낸다.
🗄️ 서버는 사용자의 정보를 확인하고 세션 객체를 생성하여 세션 id를 쿠키 (요청헤더)에 담아서 보낸다.
👨🏻💻 또다시 요청을 보낸다. 이번엔 쿠키에 세션 id가 담겨있다.
즉, 서버가 사용자의 로그인 상태를 기억하고 있는 것입니다. 따라서 세션 기반 인증은 stateful 합니다. 당연히 장단점은 존재합니다.
장점
- 클라이언트가 임의로 정보를 조작하기 어렵습니다.
- 중간에 탈취를 당해도 서버의 정보와 대조를 하기에 비교적 안전합니다.
- 서로 다른 기기에서 중복 로그인 시 인가를 취소할 수 있습니다.
단점
- 클라이언트수가 많을 경우 메모리 혹은 DB의 부하가 올 수 있습니다.
- DB가 여러개일 경우 DB간 세션을 공유하는 것이 어려워집니다.
🔑 토큰 기반 인증
그럼 다시 jwt로 돌아와서 토큰 기반 인증에 대해 얘기해보겠습니다!
토큰 방식은 세션과 다르게 stateless합니다. 즉, 서버가 사용자의 로그인 여부에 대한 정보를 기억하고 있지 않습니다. 작동반식은 이렇습니다.
👨🏻💻 사용자가 로그인 요청을 보낸다.
🗄️ 암호키를 이용해 jwt 토큰을 생성해서 보낸다.
👨🏻💻 브라우저에 token을 저장한다. 또다시 서버에 토큰과 함께 요청을 보낸다.
🗄️ 암호키로 token을 해석하여 로그인 정보가 일치하고 유호기간이 남아있으면 인가를 허가한다.
그렇다면 jwt token은 어떻게 생겼냐! 하는걸 한 번 봐보겠습니다.
payload에는 누가 누구에게 토큰을 발급했는지, 언제까지 유효한지, 닉네임은 무엇인지 등등 사용자가 서버에 알리고자 하는 정보들(Claim)이 들어있습니다. 그런데 이거 척 봐도 클라이언트가 조작하면 웁스바리.. 되게 생겼습니다. 이를 해결하고자 하는게 헤더인데요!
헤더에는 서명 에 사용할 키, 토큰 유형 (jwt를 사용하는 경우 무조건 jwt로 설정이 됩니다.), 서명 암호화 알고리즘의 정보가 담겨 있습니다. 그리고 이 알고리즘은 서버의 비밀키를 이용한 것이죠. 즉, 서버의 비밀키가 없으면 서명부분(signature)를 임의로 생성할 수 없습니다. 여기에서 만들어보면서 시험해 볼 수 있습니다.
근데 여기서 잠깐.. 또.. 딱 봐도 토큰을 누군가 탈취해가면 이거 큰일나게 생겼습니다.
이를 해결하기 위해 등장한 것이 바로 access token
과 refresh token
인데요!
🎫 access token & refresh token
로그인에 성공하면 브라우저는 access token과 refresh token 두가지를 발급받게 되는데요! Access token은 그 수명이 굉장히 짧습니다. 그래서 금방 금방 만료가 됩니다. 그렇게 되면 브라우저는 서버에 refresh token을 보내게 됩니다.
이 refresh token
은 긴 수명기간을 가지고 있는데요! 이 친구를 제시하면 서버에서 다시 access token을 발급해줍니다. 그러면 누군가가 access token을 탈취해도 금방 만료가 되어 사용할 수 없는 것입니다.
🦹♂️ 그렇다고 포기할테냐..? 어림도 없지 refresh token 탈취..!! 하면 어떻게 하느냐. 바로 Refresh Token Rotation을 사용합니다. access token 재발급시 매번 refresh token도 새로 발급받는 것입니다.
장점
- 서버가 기억하고 있지 않아도 되기때문에 서버에 부담이 적습니다.
- 구현이 쉽고 빠릅니다.
단점
- 상대적으로 보안이 취약합니다.
🔑 그래서 뭐 어떡하라고..?
저는 프로젝트에 jwt를 이용해 구현하기로 했습니다. 작은 사이드 프로젝트인 만큼 빠르게 구현하고 싶었기 때문입니다. 그러나 보안에 대한 취약점을 고려하지 않고 마구마구 코딩을 한다면.. 꽤나 무능한 개발자같고.. 그래서 고려해야 할 보안 사항들을 한 번 적어봅시다!
1. XXR (Cross-Site Scripting) 공격
크로스 사이트 스크립팅은 부정한 HTML 태그나 javascript를 동작시키는 공격입니다. html이 동적으로 생성될 때 가장 큰 문제가 생길 수 있는데요! 이때 폼에 악성 스크립트를 설치해두면 아이디나 패스워들르 그대로 탈취당할 수도 있고 .. 또 악성 스크립트를 사용해 쿠키를 탈취할 수도 있습니다.
2. CSRF(Cross-Site Request Forgery) 공격
크로스 사이트 리퀘스트 포저리는 유저가 의도하지 않은 개인 정보나 설정 정보등을 공격자가 설치해둔 함정에 의해 어떤 상태를 갱신하는 처리를 강제로 실행시키는 공격입니다. 이게 무슨 말이냐.. 하면! 로그인을 하고 인가를 받은 유저가 자기가 하지도 않은 행동을 하게 한다는 것입니다. 예를 들어 인증된 유저만 사용할 수 있는 게시판에 글을 쓴다던가.. 하는 것입니다.
아무튼.. 토큰 기반 인증 방식은 브라우저에 저장을 하게 되는데요! 이때 저장하는 위치가 보안에 큰 영향을 끼치는 것 같습니다.
🔑 브라우저의 어디에 저장을 해야할까?
바로 cookie
와 localStorage
그리고 secure httpOnly cookie
가 있습니다. 그러나 둘 모두 위의 공격에 취약할 수 있습니다.
- localStorage 저장 반식
딱 봐도 취약해보입니다. Javascript의 Window 인터페이스를 통해 접근이 가능합니다. 그러면..? XSS 공격개시.. javascript를 동작시켜서 탈취를 할 수 있습니다.
- 쿠키 저장 방식
문제는 쿠키도 javascript 내 글로벌 변수로 읽기 / 쓰기가 가능하다는 것입니다. 역시나 XSS 공격에 취약합니다. 또 CSRF 공격에도 취약합니다. 유저 권한으로 정보를 가져오거나 유저인 척 할 수 있습니다.
그러나! 쿠키에 Refresh token을 저장하고 Access Token을 또 새로 받아와 사용하는 경우 CSRF 공격은 방어 할 수 있습니다. Refresh token 탈취해 서버에 Access Token을 요청해도 응답은 사용자가 받기 때문입니다.
- secure httpOnly cookie 저장 방식
브라우저에 쿠키로 저장되는건 같지만 javascript로 접근은 불가능합니다. 오직 https 접속에서만 동작을 하게 됩니다. 따라서 XSS 공격을 방어할 수 있습니다. 또한 Refresh Token Rotation 방식을 사용하면 마찬가지로 CSRF 공격도 방어할 수 있습니다.
대신 쿠키에 세션 id, Access Token은 저장하면 안됩니다. 또 쿠키 값에 접근은 불가능하지만 XSS의 취약점을 노려 API를 요청하면 쿠키의 값도 함께 보내져 유저인 척 하는 것은 가능할 수 있습니다.
🔑 종합적인 결론
드디어 결론입니다. 정답은 없는 것 같습니다만 (샤라웃 투 klloo) 저는 secure httpOnly cookie
에 Refresh Token
을 저장해 CSRF 공격을 방어하도록 구현을 하게 될 것 같습니다! 그리고 XSS 취약점에 대한 대응은 클라이언트와 서버에서 추가적으로 구현을 해주어야 할 것 같습니다.
참고 링크
- https://jwt.io/introduction
- https://youtu.be/1QiOXWEbqYQ
- https://klloo.github.io/session-jwt/
- https://blog.bizspring.co.kr/%ED%85%8C%ED%81%AC/jwt-json-web-token-%EA%B5%AC%EC%A1%B0-%EC%82%AC%EC%9A%A9/
- https://m.yes24.com/Goods/Detail/15894097
- https://velog.io/@yaytomato/프론트에서-안전하게-로그인-처리하기#-브라우저-저장소-종류와-보안-이슈