Next.js 에서 Auth하기(JWT를 안전하게 다루는 법)

정현석·2020년 11월 27일
24

Auth를 구현할 때 세션이 아니라 JWT를 이용하는 경우 많은 예제들이 localStorage를 이용한다.

이러한 방식은 다음과 같은 경우 문제가 되지 않을 수 있다.

  • Access Token을 탈취할만한 XSS 공격을 일으키기 어려운 경우(ex. 다른 유저가 생성한 정보를 볼 일이 없는 경우)
  • Access Token을 탈취한다고 해도 치명적인 문제를 일으킬 수 없는 경우(ex. 민감하지 않은 정보에 대한 ReadOnly 권한)

이런 경우는 토큰의 수명만 적절히 설정해도 거의 문제가 없다시피하다. 하루 한번 로그인, 한달에 한번 로그인, 심지어는 평생 로그인이어도 괜찮을 수 있다.

그러나 이러한 특수한 경우를 제외하고는 조금 더 나은 방식으로 JWT를 저장해야만 한다.

저장소에 따른 보안 문제에 대한 내용은 @yaytomato 님의 다음 글에 자세히 설명되어있다.

https://velog.io/@yaytomato/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%90%EC%84%9C-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

중요한것은 XSS는 어떤 방식으로 저장하던 방지해야만 한다는 것이다.

이상적인 JWT 핸들링 방법 (by 하수라)

https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/

위 게시물은 하수라에서 추천하는 방식이다. Access Token은 메모리에 저장하고, Refresh Token은 HttpOnly 쿠키에 저장하는 것이다.

만약 이런 단어가 어렵다면 생활코딩의 'WEB' 수업을 들어보길 바란다.

Velog의 JWT 핸들링 방법

현재 게시물이 올라와있는 이 Velog는 어떤 방식으로 JWT를 다루고 있을까?

깃헙을 분석한 결과 Refresh Token과 Access Token 모두를 HttpOnly 쿠키에 저장하는 방식을 이용하고 있다. @yaytomato 님의 글에 나오는 세번째 케이스와 같다.

두 방식의 차이점

결론부터 말하자면 하수라의 방식이 조금 더 JWT의 원래 성격과 더 알맞다. 물론 그것이 무조건 뛰어나다는 것은 아니다. 그 이유를 알아보자.

JWT가 세션에 비해서 가지는 장점으로는, 다양한 마이크로 서비스를 이용하는데에 유용하다는 것이다. 예를 들어 Velog에 로그인 한 경우, 현재 보유중인 Access Token을 통해 Velog와 전혀 다른 저장소와 도메인을 갖지만, Velog와 함께 작동하는 또 다른 서비스에도 사용 할 수 있다. 그리고 그 서비스가 10개든 100개든 1000개든 토큰의 secret만 알고 있다면 어떠한 문제 없이 권한을 인증할 수 있다.

현재 Velog는 velog.io 이외의 도메인에서 이용하지 않고, api서버 또한 같은 도메인에서 제공하기 때문에 굳이 메모리 방식을 사용하고 있지 않다.

그러나 만약 api서버를 프론트엔드 어플리케이션 외부에 분리해야하고, 그 도메인이 다르거나 한다면 하수라의 방식을 이용하는것이 좋다.

Next.js 에서 하수라 방식 구현하기

내가 제작 중인 프로젝트는 Next.js 프론트 엔드 앱과, Apollo GraphQL API 서버의 도메인이 분리되어있다.(참고로 서브도메인끼리는 쿠키를 공유하지만, 나의 프로젝트는 서브도메인관계조차 아니며, 여러 API 서버를 사용한다.)

호스팅은 Vercel에서 하고 있는데, Next.js 프론트 엔드 소스의 pages/api 폴더 내부에 Serverless Function을 작성하면 프론트 엔드 앱에 API를 쉽게 붙일 수 있다.

물론 GraphQL API를 pages/api 내부에 전부 작성하는 것도 하나의 방법이고, 이렇게 하면 굳이 복잡하게 구현하지 않아도 된다. 쿠키에서 바로 읽어버리면 그만이니까.

그러나 서버를 분리하자면 다음과 같은 순서를 따라야한다. 참고로 이 로직은 Velog의 서버를 많이 참고했다.

구글 OAuth2 를 기준으로 설명하겠다.

  1. SNS 로그인 페이지에서 사용자가 구글 로그인을 클릭. OAuth2 URL로 넘어감
  2. 사용자가 구글 로그인을 행함
  3. Next.js API URL로 Redirect되며 유저의 구글 이메일, 유저의 구글 프로필, 구글에서 발급한 Code(혹은 Token)를 전송
  4. Next.js API는 전달된 정보를 바탕으로 Refresh Token을 생성해야함. Refresh Token에는 유저의 ID와 IAT(토큰 생성시각), Expiry등의 정보가 기입됨(아래 참고). 이렇게 넘어온 구글 이메일이 DB에 존재하지 않는 계정이면 새로 생성하거나, 이미 존재하는 경우 해당 레코드를 읽어옴.
  5. Refresh Token을 HTTP 헤더에 Set-Cookie 함. 이제 Next.js의 getInitialProps(9.0 이상에서는 getStaticProps, getServerSideProps)를 통해 Next.js의 서버단에서도 이를 확인할 수 있음.
  6. API에서 연결을 끝내며 코드 302로 "/" 등의 페이지로 리디렉션함.
  7. 해당 페이지의 getInitialProps에서 로그인 되었음을 알림.
  8. 클라이언트 단에서 로그인은 되었으나 access Token이 아직 메모리에 저장되어 있지 않음을 감지. Refresh Token을 통해 Access Token을 발급해주는 Next.js API를 콜.
  9. Next.js의 API에서 Access Token을 발급해주고, 이를 메모리에 저장하며 완료

이렇게 구글 로그인 이후 총 2번의 API콜을 통해 하수라의 방식을 구현할 수 있음.

마치며

로그인 여부 확인에 쿠키를 이용할 수 있다는 것은 Next.js에 굉장히 유용함. 특히나 Next.js의 경우 SSR을 하려고 한다면 Refresh Token이 쿠키에 있는지를 확인하면서 UI를 알맞게 그릴 수 있다는 장점이 있음.

이외에도 매우 다양한 방법이 있음.

Next.js API를 프록시 API처럼 이용하여 HttpOnly 쿠키를 통해 인증
https://maxschmitt.me/posts/next-js-http-only-cookie-auth-tokens/

혹은 next-auth나 next-iron-session 과 같은 라이브러리를 사용하는것도 좋음.

next-auth의 경우 SNS로그인이 매우 쉽고,

next-iron-session은 Ruby on Rails에서 사용하는 방식과 비슷함.

다음번엔 이 두가지 라이브러리들의 방식을 분석해보아야겠음.

참고

참고로 Velog의 Refresh Token Payload는 다음과 같다. token_id 항목은 DB에서 허용하는 token 리스트를 따로 관리하기 때문에 필요하다.

{
  "user_id": "XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "token_id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "iat": 1606448678,
  "exp": 1609040678,
  "iss": "velog.io",
  "sub": "refresh_token"
}

Access Token Payload는 다음과 같다.

{
  "user_id": "XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "iat": 1606461546,
  "exp": 1606465146,
  "iss": "velog.io",
  "sub": "access_token"
}

https://jwt.io/

위 사이트를 통해 각자 자신의 쿠키 정보를 decode하여 볼 수 있다.

profile
데이터 사이언스 공부중

1개의 댓글

comment-user-thumbnail
2021년 4월 18일

안녕 하세요~ 프론트앤드 초보 개발자 입니다. Next js로 구현 테스트중 입니다만,
Next.js 에서 Auth하기(JWT 사용)... 샘플 코드를 받아볼수 있는지요?
받아볼수 있으면 많은 도움이 될거 같습니다...
메일 주소는 pkbond1@hanmail.net 입니다.

답글 달기