Session에서 Cool해보이는 JWT로 바꾸며 겪은 일에 대해 적어보았습니다
그 거창한 이름이 끌려버렸다
출처 : 기존의 Web Session방식
state
가 less한 상태. 기존에 구현한 Session
은 로그인 하면
Session Id
가 생성되어 Front End에 쿠키로 전달되고, 그 난수가 Session
에 저장된다.withCredentials
설정을 통해 함께 전달된 쿠키를 Session
에 저장된 정보와 확인한다.이처럼 기존의 방식은 Session을 유지해야 한다는 점, 서버 확장성이 낮다는 점에 JWT
방식으로 바꾸게 되었다.
사용자가 많아질 테니까?
기존에 passport-local
을 통해 처리했던 내용을 유지하면서, 먼저 { session: false }
Option을 추가했다. 기존의 Session방식을 없애고 id와 password를 필요로 하는 기존의 전략을 유지하면서 올바른 user객체가 return 됐을 때 jwt를 만드는 방식으로 진행했다.
jsonwebtoken
라이브러리를 통해 쉽게 jwt를 만들 수 있었다. jwt.sign()
을 통해 만든 token을 console.log로 찍어봤다. 그리고 별거 아니라고 생각한 순간 의문이 똬리를 틀고 있었다.
아니 그래서 이걸 어떻게 보내야 하는데?
너무도 쉽게 봤다.
먼저 JWT 인증방식에 대해 이해가 필요했다.
jwt예제를 보면 postman을 통해서, 발급한 token을 passport-jwt
의 전략으로 체크하고 마무리한다. 대부분 ExtractJwt.fromAuthHeaderAsBearerToken
을 사용해서 token을 추출하고 콜백함수로 decoding한 payload를 받는다.
여기서부터 의문은 시작되었다.
지금껏 공부해올 때 적합한, 적확한 방법을 사람들은 말해주었다. 그리고 처음으로 어떻게 해야할지 모르겠는 때가 왔다. 아마 쉽게 검색하고 따라치는 Coder였기 때문이 아닐까
먼저 토큰을 보내는 방법은 두 가지 정도 있다.
그리고 선택하는 것은 자유다. 출처
나는 server에서 client로 jwt를 Cookie로 보내기로 했다.
이에대한 답변을 하기 위해서는 먼저 JWT를 저장하는 방법에 대해 말해야 한다.
로그인 이후 추후 요청에 있어서 토큰을 함께 보내줘야 하기 때문에 Client는 토큰을 유지하고 있어야 한다. 크게 방법은 LocalStorage와 Cookie가 있다.
그러나 검색 결과 대부분의 글에서 LocalStorage 저장방식을 금지한다.출처1, 출처2
왜 금지하는지 살펴보면 can be vulnerable to cross-site scripting (XSS) attacks.
즉 XSS 공격에 취약하다고 말한다.
아니 그러면 XSS공격이 뭔데?
XSS공격은 script가 삽입되어야 하기 때문에 입력가능한 Form이 필터링이 제대로 이뤄지지 않으면 발생한다. 또는 url에 script를 보내 실행할 수 있다면 XSS공격이 이뤄질 수 있다.
React에서는 신뢰하지 않는 데이터에 대해 escape와 encode를 지원한다. 출처1, 출처2
그러나 모두 여전히 XSS공격이 가능하다고 말한다.
아 그러면 LocalStorage 아닌 것 같아😭
쿠키.
먼저 Next로 SSR 웹 어플리케이션을 진행중이었기 때문에 페이지가 변경되면 redux의 상태값이 초기화 돼버렸다. 따라서 token을 저장할 마땅한 공간을 Cookie라고 생각했고 이에따라 Cookie로 JWT를 보내 쉽게 처리할 심산이었다.
그러나, 쿠키 또한 문제가 도사리고 있었다.
쿠키 또한 Javascript에서 Global 변수이기 때문에 접근이 가능하다는 점이다. 따라서 여전히 LocalStorage의 저장방식과 같이 XSS위험이 도사리고 있었다.
그럼 방법이 없어?
그것은 httpOnly 설정이다. httpOnly Cookie는 client-side에서 데이터에 접근하는 것을 막는다. 특히 서버를 제외하고 다른 곳에서의 접근을 막는 쿠키이다.
그러나 이 또한 XSS공격을 통해 사용자로 둔갑하여 정보를 가져올 수 있다. 출처
또한 쿠키 사용시 가장 주목해야할 부분은 CSRF공격이다. 출처1,출처2
CSRF? 이건 뭘까요? 🧐
CSRF(Cross-Site Request Forgery)공격이란 신뢰할 수 있는 사이트에서 사용자의 인증이 되어있을 때, 사용자의 웹 브라우저가 악의적인 웹사이트나 이메일, 메세지 등에 의해 원치않는 action을 수행하는 공격이다. 출처
이러한 공격은 웹 브라우저의 request가 자동적으로 세션 쿠키를 포함한 모든 쿠키를 자동적으로 포함하기 때문에 발생한다. 따라서 token을 cookie에 저장하는 지금의 JWT 인증방식은 CSRF공격에 취약점을 드러낸다.
이 부분은 더욱 공부해야하는 부분입니다. 여러가지 방법이 있으며, 해당 방법의 장단을 설명하기에 글이 너무 길어질 것 같아 넣지 않았습니다. 또한 올바르지 않은 내용이나 추가 언급해주실 부분이 있으시면 피드백 부탁드립니다.🙏🏼
CSRF공격을 막기위한 가장 기본적인 방법으로 Referer check가 있다. Referer헤더는 현재 요청된 페이지의 링크 이전의 웹 페이지 주소를 포함하여 어디로부터 접속된 것인지 파악할 수 있게 해준다. 그러나 XSS취약점이 있는 경우 여전히 CSRF공격에 처해있다.
express에서 referer를 체크하는 middleware
exports.refererCheck = (req, res, next) => { if (req.headers.referer === 'http://localhost:3000/'){ next(); } else { res.status(403).send('Referer Error'); } }
말 그대로 CSRF를 방지하기 위한 토큰이다. 구현 방법에는 여러가지가 있겠지만, 기본적인 틀은 다음과 같다.
1. 사용자가 form이 있는 페이지에 접근한다.(get method 이외의 요청)
2. 서버가 CSRF Token을 발급하고 token을 session에 저장한다.
3. 서버는 사용자가 요청한 page의 hidden Input에 해당 값을 넣어 응답을 보낸다.
4. 이후 사용자의 요청시 form의 hidden값과 session의 값을 비교한다.
5. 비교 결과에 따라 처리를 결정한다.
server가 난수값을 사용자의 form에 부여하고 해당 form의 요청이 들어왔을 때 비교하는 형식으로 CSRF 공격을 막는 방법이다. 그러나 Session을 사용한다는 점에 마음이 걸렸다.
Session을 사용하지 않기위해 JWT로 변경했는데 다시 Session이라니.
AccessToken
AccessToken
은 resource
에 직접 접근할 수 있는 필수적인 정보를 담음 토큰이다. 서버는 토큰에 들어있는 정보를 활용하여 사용자가 권한 유무 등을 결정내릴 수 있다. 또한 짧은 생명주기를 갖는다.
RefreshToken
RefreshToken
은 새로운 AccessToken
을 발급받을 수 있는 정보를 가지는 토큰이다. AccessToken
이 만료될 때 새로운 토큰을 얻기위해 사용되며 비교적 긴 생명주기를 갖는다.
그렇다.
AccessToken
의 유효기간이 짧기 때문에 CSRF공격 가능성을 낮춘다는 말이다. 여전히 AccessToken
이 탈취되면 발생하는 보안문제는 동일하다. 따라서 Server-User
모두 XSS방어에 주의를 기울여야 한다.
지금까지 배운 모든 것을 생각해서 이것저것이 섞인 구조를 만들었다.
최초 로그인시 다음과 같다.
AccessToken
와 RefreshToken
을 이용했다. 두 토큰 모두 JWT이지만 payload는 다르다. 사용자는 로그인시 두 토큰을 모두 받고 RefreshToken
은 쿠키에 저장하고 AccessToken
을 이용해 이후 CRUD작업과 같은 서버의 자원을 활용하는 작업을 진행한다.
그러나 next-redux-wrapper
는 페이지 이동시 데이터가 휘발되기 때문에 AccessToken
을 계속 유지하기 힘든점을 비춰 추가적인 구조가 필요했다. 아래와 같다.
마치 CSRF토큰처럼 AccessToken
을 사용한다. 프로젝트의 Feed
페이지 내에서는 글을 올리고 댓글을 작성하고, 글을 삭제하는 등 다양한 작업을 한다. 마치 SPA처럼 작동하기 때문에 페이지 내에서 첫 로드 때 받은 AccessToken
을 만료기간 내에 사용할 수 있다. 더불어 만료시 자동으로 새로운 토큰을 가져오도록 사용자의 편리상 구현했다.
글을 무려 3일간 썼다. 쓰다보니 이해 안가는 부분이 나왔고, 해당 부분을 해결하고 사고의 흐름에 맞게 글쓰려 노력했다. 그러나 여전히 XSS공격을 신경써야 하고, 여전히 미진한 부분이 넘친다. 프로젝트를 통해 처음으로 보안에 대해 배웠고 신경썼다.
과연 이러한 방식이 올바른 방식일지에 여전히 의문이 들지만, 처음에는 어떤 로그인방식이 좋을까에 대한 의문으로부터 기초적인 보안문제에 도달하기까지 나의 생각을 넓히기에 좋은 경험이었음에는 확실하다.
말씀하신대로 로그인 후 accesstoken을 다른 라우터로 갈 때 마다 새로 받아오면 인증이 유지가 될거같은데, 새로고침이나 브라우저 탭을 껐다 켰을 때는 어떻게되나요? 지역변수에 토큰이있으면 새로고침시 토큰이 날아갈 것 같은데 그때마다 다시 로그인을 해야하는 건가요?
덕분에 많은 인사이트를 얻었습니다 감사합니다 :)