JWT 로그인방식 구현하기 (feat. session에서 jwt로)

개발자 왜?전·2020년 11월 9일
69
post-thumbnail

Session에서 Cool해보이는 JWT로 바꾸며 겪은 일에 대해 적어보았습니다




왜?


그 거창한 이름이 끌려버렸다



Stateless


출처 : 기존의 Web Session방식

state가 less한 상태. 기존에 구현한 Session은 로그인 하면

  1. Session에 로그인 정보를 저장하고 그 정보를 이용한다.
  2. 로그인을 하면 Session Id가 생성되어 Front End에 쿠키로 전달되고, 그 난수가 Session에 저장된다.
  3. 이후 클라이언트의 요청이 있을 때 마다 withCredentials 설정을 통해 함께 전달된 쿠키를 Session에 저장된 정보와 확인한다.
  4. 이후 확인이 완료되면 DB에서 해당 유저정보를 가져와 Request의 user에 값을 넣어준다.

이처럼 기존의 방식은 Session을 유지해야 한다는 점, 서버 확장성이 낮다는 점에 JWT방식으로 바꾸게 되었다.

사용자가 많아질 테니까?



어떻게?


passport-local


기존에 passport-local을 통해 처리했던 내용을 유지하면서, 먼저 { session: false } Option을 추가했다. 기존의 Session방식을 없애고 id와 password를 필요로 하는 기존의 전략을 유지하면서 올바른 user객체가 return 됐을 때 jwt를 만드는 방식으로 진행했다.

passport-jwt & jsonwebtoken


jsonwebtoken 라이브러리를 통해 쉽게 jwt를 만들 수 있었다. jwt.sign()을 통해 만든 token을 console.log로 찍어봤다. 그리고 별거 아니라고 생각한 순간 의문이 똬리를 틀고 있었다.


아니 그래서 이걸 어떻게 보내야 하는데?


너무도 쉽게 봤다.





배워야 할 게 많구나😁


JWT 인증방식이 뭐야?

먼저 JWT 인증방식에 대해 이해가 필요했다.

사진출처

  1. 먼저 브라우저에서 Login요청을 한다.
  2. 서버에서 JWT를 발급한다.
  3. 발급한 JWT를 브라우저로 보낸다.
  4. 이후 요청시 발급받은 JWT를 함께 보낸다.
  5. 서버에서 JWT에 포함된 Signature를 확인 후 user정보를 request에 담아준다.
  6. 서버에서 브라우저의 요청을 처리한다.
  7. 브라우저로 response를 보낸다.

jwt예제를 보면 postman을 통해서, 발급한 token을 passport-jwt의 전략으로 체크하고 마무리한다. 대부분 ExtractJwt.fromAuthHeaderAsBearerToken을 사용해서 token을 추출하고 콜백함수로 decoding한 payload를 받는다.

여기서부터 의문은 시작되었다.



JWT토큰을 보내는 방법


지금껏 공부해올 때 적합한, 적확한 방법을 사람들은 말해주었다. 그리고 처음으로 어떻게 해야할지 모르겠는 때가 왔다. 아마 쉽게 검색하고 따라치는 Coder였기 때문이 아닐까

먼저 토큰을 보내는 방법은 두 가지 정도 있다.

  1. response로 보내는 법
  2. cookie로 보내는 법

그리고 선택하는 것은 자유다. 출처


나는 server에서 client로 jwt를 Cookie로 보내기로 했다.

이에대한 답변을 하기 위해서는 먼저 JWT를 저장하는 방법에 대해 말해야 한다.



JWT토큰을 저장하는 방법, LocalStorgae


로그인 이후 추후 요청에 있어서 토큰을 함께 보내줘야 하기 때문에 Client는 토큰을 유지하고 있어야 한다. 크게 방법은 LocalStorage와 Cookie가 있다.

그러나 검색 결과 대부분의 글에서 LocalStorage 저장방식을 금지한다.출처1, 출처2

왜 금지하는지 살펴보면 can be vulnerable to cross-site scripting (XSS) attacks.XSS 공격에 취약하다고 말한다.

아니 그러면 XSS공격이 뭔데?



XSS공격


출처

  1. 공격자는 script가 삽입될 수 있는 취약점이 있는 사이트를 물색한다.
  2. 공격자는 해당 웹사이트에 사용자의 쿠키와 같은 정보를 훔치는 악성 script를 삽입한다.
  3. 사용자가 해당 웹사이트에 접속할 때마다 악성 script가 작동된다.
  4. 사용자의 정보가 공격자로 보내진다.

XSS공격은 script가 삽입되어야 하기 때문에 입력가능한 Form이 필터링이 제대로 이뤄지지 않으면 발생한다. 또는 url에 script를 보내 실행할 수 있다면 XSS공격이 이뤄질 수 있다.

React에서는 신뢰하지 않는 데이터에 대해 escape와 encode를 지원한다. 출처1, 출처2

그러나 모두 여전히 XSS공격이 가능하다고 말한다.

  1. dangerouslySetInnerHTML을 사용한 공격
  2. a.href 속성을 통해 공격
  3. props 조작을 통한 공격

아 그러면 LocalStorage 아닌 것 같아😭




쿠키.

먼저 Next로 SSR 웹 어플리케이션을 진행중이었기 때문에 페이지가 변경되면 redux의 상태값이 초기화 돼버렸다. 따라서 token을 저장할 마땅한 공간을 Cookie라고 생각했고 이에따라 Cookie로 JWT를 보내 쉽게 처리할 심산이었다.


그러나, 쿠키 또한 문제가 도사리고 있었다.


쿠키 또한 Javascript에서 Global 변수이기 때문에 접근이 가능하다는 점이다. 따라서 여전히 LocalStorage의 저장방식과 같이 XSS위험이 도사리고 있었다.

그럼 방법이 없어?




그것은 httpOnly 설정이다. httpOnly Cookie는 client-side에서 데이터에 접근하는 것을 막는다. 특히 서버를 제외하고 다른 곳에서의 접근을 막는 쿠키이다.

그러나 이 또한 XSS공격을 통해 사용자로 둔갑하여 정보를 가져올 수 있다. 출처

또한 쿠키 사용시 가장 주목해야할 부분은 CSRF공격이다. 출처1,출처2

CSRF? 이건 뭘까요? 🧐



CSRF공격


CSRF(Cross-Site Request Forgery)공격이란 신뢰할 수 있는 사이트에서 사용자의 인증이 되어있을 때, 사용자의 웹 브라우저가 악의적인 웹사이트나 이메일, 메세지 등에 의해 원치않는 action을 수행하는 공격이다. 출처

이러한 공격은 웹 브라우저의 request가 자동적으로 세션 쿠키를 포함한 모든 쿠키를 자동적으로 포함하기 때문에 발생한다. 따라서 token을 cookie에 저장하는 지금의 JWT 인증방식은 CSRF공격에 취약점을 드러낸다.




그럼 어떻게 해야 하는거야?


이 부분은 더욱 공부해야하는 부분입니다. 여러가지 방법이 있으며, 해당 방법의 장단을 설명하기에 글이 너무 길어질 것 같아 넣지 않았습니다. 또한 올바르지 않은 내용이나 추가 언급해주실 부분이 있으시면 피드백 부탁드립니다.🙏🏼


Referer Check


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 token


출처

말 그대로 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이라니.



RefreshToken, AccessToken


출처

  • AccessToken
    AccessTokenresource에 직접 접근할 수 있는 필수적인 정보를 담음 토큰이다. 서버는 토큰에 들어있는 정보를 활용하여 사용자가 권한 유무 등을 결정내릴 수 있다. 또한 짧은 생명주기를 갖는다.

  • RefreshToken
    RefreshToken은 새로운 AccessToken을 발급받을 수 있는 정보를 가지는 토큰이다. AccessToken이 만료될 때 새로운 토큰을 얻기위해 사용되며 비교적 긴 생명주기를 갖는다.

그렇다.
AccessToken의 유효기간이 짧기 때문에 CSRF공격 가능성을 낮춘다는 말이다. 여전히 AccessToken이 탈취되면 발생하는 보안문제는 동일하다. 따라서 Server-User 모두 XSS방어에 주의를 기울여야 한다.




결국 나는🤔?


지금까지 배운 모든 것을 생각해서 이것저것이 섞인 구조를 만들었다.

최초 로그인시 다음과 같다.

AccessTokenRefreshToken을 이용했다. 두 토큰 모두 JWT이지만 payload는 다르다. 사용자는 로그인시 두 토큰을 모두 받고 RefreshToken은 쿠키에 저장하고 AccessToken을 이용해 이후 CRUD작업과 같은 서버의 자원을 활용하는 작업을 진행한다.

그러나 next-redux-wrapper는 페이지 이동시 데이터가 휘발되기 때문에 AccessToken을 계속 유지하기 힘든점을 비춰 추가적인 구조가 필요했다. 아래와 같다.

마치 CSRF토큰처럼 AccessToken을 사용한다. 프로젝트의 Feed 페이지 내에서는 글을 올리고 댓글을 작성하고, 글을 삭제하는 등 다양한 작업을 한다. 마치 SPA처럼 작동하기 때문에 페이지 내에서 첫 로드 때 받은 AccessToken을 만료기간 내에 사용할 수 있다. 더불어 만료시 자동으로 새로운 토큰을 가져오도록 사용자의 편리상 구현했다.




마무리


글을 무려 3일간 썼다. 쓰다보니 이해 안가는 부분이 나왔고, 해당 부분을 해결하고 사고의 흐름에 맞게 글쓰려 노력했다. 그러나 여전히 XSS공격을 신경써야 하고, 여전히 미진한 부분이 넘친다. 프로젝트를 통해 처음으로 보안에 대해 배웠고 신경썼다.
과연 이러한 방식이 올바른 방식일지에 여전히 의문이 들지만, 처음에는 어떤 로그인방식이 좋을까에 대한 의문으로부터 기초적인 보안문제에 도달하기까지 나의 생각을 넓히기에 좋은 경험이었음에는 확실하다.

profile
하고 싶어 개발하는, 능동개발자

4개의 댓글

comment-user-thumbnail
2021년 5월 9일

덕분에 많은 인사이트를 얻었습니다 감사합니다 :)

답글 달기
comment-user-thumbnail
2022년 4월 14일

감사합니다. 처음으로 jwt를 활용해서 로그인 구현중인데, 쉽지않네요.. 정리 감사합니다~

답글 달기
comment-user-thumbnail
2022년 4월 27일

말씀하신대로 로그인 후 accesstoken을 다른 라우터로 갈 때 마다 새로 받아오면 인증이 유지가 될거같은데, 새로고침이나 브라우저 탭을 껐다 켰을 때는 어떻게되나요? 지역변수에 토큰이있으면 새로고침시 토큰이 날아갈 것 같은데 그때마다 다시 로그인을 해야하는 건가요?

1개의 답글