(Security) JWT 인증 방식

2019/05/17

유저 인증

유저 인증이란 무엇일까? 유저 인증은 어떤 서비스에 속한 유저가 정상적으로 로그인을 하여 서비스 API를 사용 할 때, 이 유저가 어떤 유저인지 서버측에서 인증을 하는 것이다. 그렇다면, 유저 인증은 어떤식으로 이루어질까? 이러한 유저 인증 방식을 잘 이해하기 위해서는 session과 쿠키, 그리고 JWT와 OAuth에 대해서 이해를 하고 왜 JWT (Json Web Token)이 유저 인증에서 편리한지에 대해서 이해할 필요가 있다.

http는 stateless 무상태성을 지향한다. 그렇기 때문에, 어떤 Request가 왔을 때, 이 Request는 자원에 대한 요청을 하지만, 이 Request를 보낸 주체가 누구인지 알 수 없다. 물론, 이 Request의 주체가 중요하지 않은 경우에는 상관 없지만, 어떤 유저가 자신의 리소스에 접근하려 한다면 이 Request는 어떤 유저가 보낸 요청인지 알아야 한다. 이 방법을 제공하기 위해서 기존에는 웹 브라우저에서는 쿠키를 사용했다. 쿠키는 브라우저 내에 저장공간에 key - value 형태로 데이터를 저장하는 방식이다. 이 쿠키에 유저에 대한 정보, 로그인 상태 등을 저장하고 서버에 요청을 http로 보낼 때, 함께 적재하여 보냈고, 서버는 적재된 정보로 이 요청이 어떤 유저 (user_id)로 부터 왔는지 식별하고 그에 맞는 데이터를 fetch 하여 돌려보내 주었다. 그러나, 문제는 client side에 저장되는 쿠키 정보 다보니 쉽게 변조가 가능했다. 즉, user_id를 변경한다거나, 권한을 수정한다거나 등을 통해서 다른 정보들을 가져갈 수 있었기 때문에 보안에 아주 취약했다. 쿠키에 정보를 넣다보니, 이 정보를 header에 실어서 보낼 때, header의 데이터 크기가 커지기 때문에, 트래픽을 더 많이 사용하게 되었다. 이러한 단점을 개선하기 위해서, session이 등장했다. session은 서버측에서 관리하기 때문에 유저가 사용하는 브라우저 보다는 중앙에서 보안을 관리해서 보안이슈에서는 쿠키보다는 좀 더 안전하다. 또, session 정보는 memory 쿠키에 저장되기 때문에 브라우저가 종료되면 삭제되게 된다. 세션을 사용할 경우, 인증 방식은 아래와 같은 순서로 진행이 된다.

세션의 동작 순서
클라이언트가 서버에 처음으로 Request를 보냄 (첫 요청이기 때문에 session id가 존재하지 않음)
서버에서는 session id 쿠키 값이 없는 것을 확인하고 새로 발급해서 응답
이후 클라이언트는 전달받은 session id 값을 매 요청마다 헤더 쿠키에 넣어서 요청
서버는 session id를 확인하여 사용자를 식별
클라이언트가 로그인을 요청하면 서버는 session을 로그인한 사용자 정보로 갱신하고 새로운 session id를 발급하여 응답
이후 클라이언트는 로그인 사용자의 session id 쿠키를 요청과 함께 전달하고 서버에서도 로그인된 사용자로 식별 가능
클라이언트 종료 (브라우저 종료) 시 session id 제거, 서버에서도 세션 제거

하지만, 이 방식은 server side에서 유저가 많아 질수록 문제가 발생한다.

  1. database 저장시, session id 매치 확인을 위해 database access 비용증가
  2. 메모리 저장시, 서버측 리소스 자원 낭비
  3. 메모리 저장시, Scale Out을 하여 여러 서버 인스턴스가 있을 경우, 세션 유지가 어려움.
  4. CORS (cross-origin resource sharing) 문제 : 단일 도메인에서만 쿠키가 사용가능하기 때문에, 도메인이 다양할 경우, 쿠키 정보를 활용할 수 없다.

위와 같은 다양한 문제 때문에 JWT (Json Web Token)이 등장하게 되었다.

JWT 토큰 등장

위와 같은 문제점들을 해결하고자 나온 것이 JWT (json web token) 이다. 이는 기존 OAuth2 를 좀 더 경량화한 인증 토큰이다. JWT는 ‘.’ 을 기준으로 3가지 구조로 정보를 구성하고 있다.

jwt constructor

각각의 토큰들은 base64로 인코딩 되어있다. 각각의 설명을 하기전에, JWT 토큰은 토큰 자체가 정보를 지니고 있다는 점이 매우 중요하다. 이는 우리가 기존에 클라이언트 사이드에 저장하던 정보를 토큰 자체가 가지게 되는 것이고, 서명이 포함되어있기 때문에 서버측에서 발급한 토큰이 아니면 리소스에 대한 권한이 없는 것으로 판단할 수 있다. 즉, 무결성을 보장할 수 있다. 이러한 토큰의 구조 덕분에 JWT는 stateless 하고 (좀 더 쉽게 생각하면, 누가 보냈는지 세션을 유지하지 않아도 되고), 서버측에서는 단지 이 토큰에 대한 무결성 검증 (비밀키를 이용한 verify 복호화)만 하면 되기 때문에 서버 리소스를 사용하는 문제 또한 해결할 수 있게 되었다. 아래에는 위에 JWT 토큰에 대한 구조들의 자세한 설명을 부연설명 하도록 하겠다.

헤더

헤더는 두가지 정보를 지니고 있다 토큰의 type과 algorithm 정보이다. 토큰의 typ은 JWT이고, alg은 주로 HMAC SHA256 혹은 RSA가 사용되는데, 이는 서명 부분에서 토큰을 검증할 때, 사용된다.

’’’ { “typ”: “JWT”, “alg”: “HS256” } ‘’’

Payload

페이로드는 key와 value로 이루어져 있고, 이 key와 value 한쌍을 Claim (클레임) 이라고 부른다. 이 페이로드 토큰에는 여러가지 클레임 정보들을 담을 수 있다. 클레임의 종류는 크게 3가지로 이루어져 있다.

  • 등록된 (registered) 클레임

등록된 클레임들은 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기 위하여 이미 정해져 있는 클레임 이름들이다. 모든 등록된 클레임들은 사용이 필수적이 아니라, 옵션이며, 포함된 클레임들의 이름은 아래와 같다.

  • iss: 토큰 발급자 (issuer)
  • sub: 토큰 제목 (subject)
  • aud: 토큰 대상자 (audience)
  • exp: 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1480849147370) 언제나 현재 시간보다 이후로 설정 되어있어야 한다.
  • nbf: Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념입니다. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않습니다.
  • iat: 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있습니다.
  • jti: JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용됩니다. 일회용 토큰에 사용하면 유용합니다.

  • 공개 (public) 클레임

  • 비공개 (private) 클레임

비공개 클레임은 서버와 클라이언트가 서로 사용하기로 합의한 정보를 의미한다. 이 부분은 서비스 내에서 사용할 클레임 정보들을 넣게 되는데, 물론 이 클레임 정보는 절대 보안이슈가 있는 정보는 넣어서는 안된다. 왜냐면 클레임 정보는 base64로 인코딩 되어있기 때문에 쉽게 디코딩 되기 때문이다.

Signature

누군가가 악의적으로 토큰을 변조하여 토큰의 무결성이 깨지지 않도록 무결성 을 보장하기 위해서 일반적으로 사용하는 방식이 서명방식이다. 서명은 해시 알고리즘을 통해 이루어지는데, JWT에서는 위의 base64로 인코딩된 Header와 payload를 비밀키로 암호화하여 생성한다. 이 경우, 토큰을 악의적으로 변조 하게 될 경우, 서버측에서는 비밀키를 통해 토큰을 복호화 하기 때문에, 만약 잘못된 비밀키로 서명을 생성하게 되면 이는 서버측에서 위조된 것을 알고 서버 리소스 접근을 허용하지 않는다.

JWT vs OAuth2

JWT와 OAuth2의 차이점은 무엇일까? JWT는 인증기반 중심으로 토큰 발행과 인증을 하게 되지만, OAuth2는 Authorization Server에서 client, service provider, user 의 통신을 위한 프레임 워크 레벨이라고 생각하면 된다.

Token Life Cycle

token-flow RFC6749 section 1.5 참조 : https://tools.ietf.org/html/rfc6749#section-1.5

토큰은 어떤 과정을 거쳐서 발행이 되고 사용될까? 위에는 OAuth2 의 토큰 발행 과정이다. 이를 참고하여 보자면, (A) 과정에서 유저는 로그인을 시도하게 되고, 서버측에서는 유저의 계정이 데이터베이스에 저장된 것과 일치하는지 확인 후, (B) 로 Access Token과 Refresh Token을 발행한다. (C) 에서는 클라이언트는 발급받은 Access Token을 Authorization Header에 넣어 Resource Server로 부터 자원을 얻는다. 이때 Resource Server에서는 Token을 비밀키를 이용하여 검증한다. 만약 비밀키를 통해 토큰의 서명이 복호화가 된다면 토큰 무결성이 검증된 것이므로, (D) Protected Resource를 제공한다. (E) 과정에서 다시 리소스를 요청할 때, 만약 Token이 만료가 되었다면, Resource Server에서는 Token이 만료되었다는 에러를 (F) 반환하게 된다. 이때 클라이언트 측에서는 (G) Refresh Token을 이용하여 (H) 새로운 Access Token을 받고, 서버측에서는 유효기간이 재 갱신된 새로운 Refresh Token을 함께 발행하거나, 또는 발행하지 않고 일정 시간 이후에는 유저가 무조건 재 로그인을 통해 인증을 받도록 해야한다.

AccessToken과 RefreshToken이란?

JWT 토큰에는 access token과 refresh token이 존재한다. access token이란 리소스에 접근하기 위한 토큰이고, refresh token은 access token을 재발행 받기 위한 토큰이다. 왜 2가지 토큰이 존재할까? 이는 Json Web Token 방식이 단점을 지니고 있기 때문이다. 그 단점이란, 사용자 측에서 토큰을 관리하기 때문에, 토큰 관리를 잘못할 경우 토큰이 노출될 수 있다는 것이다. 이를테면, 사용자 A가 공용 컴퓨터에서 로그인을 하고 Access token을 받았다고 한다면, 그 토큰을 누군가가 해당 컴퓨터에서 얻어낼 수 있고, 이를 통해 다양한 사용자 리소스 API에 접근할 수도 있다. 이러한 보안적인 문제를 막기 위해서 토큰 발행시에 Access Token은 만료되는 짧은 토큰으로 지정하는 것이 권장된다. 이는 위에서 ‘등록된 클레임’에서 ‘exp’ 클레임에 만료시간을 지정하면 해결된다. 만약 토큰이 만료된다면, 토큰 발행시 함께 발행되는 refresh token을 통해서 access token을 다시 받아야 한다. 이때, refresh token은 access token에 비해 만료 시간이 길다.

토큰의 만료시간 정책

그렇다면, 만료시간은 몇분으로 설정하는 것이 좋을까? 서비스의 특성상 이는 매우 다를 수 있기 때문에, JWT 토큰을 인증 방식으로 사용하는 팀의 경우 논의가 필요할 것이다. 일반적으로는 Access Token의 유효시간은 약 1시간 그리고 Refresh Token의 유효 시간은 약 2주일~1달 사이로 책정한다. 만료시간에 따른 장단점은 무엇일까?

  짧은 만료 시간을 설정
장점 토큰이 혹시 탈취 되더라도 빠르게 만료가 된다. 즉, 보안성이 강화된다.
단점 토큰이 만료가 되면, 해당 토큰으로 서비스에 접근할 수 없다. 보통 401 Unauthorized http error가 발생하게 된다. 이 경우, 사용자가 오랫동안 상주할 경우, 인증이 만료되어 로그인 창으로 바뀔 수 있다.
  긴 만료 시간을 설정
장점 사용자가 로그인을 자주 하지 않아도 된다. 사용 편의성이 증대된다.
단점 토큰이 탈취될 경우 만료되기 전까지 사용이 가능해진다. 보안성이 약화된다.

위와 같은 장단점을 고려하여 토큰 인증 방식을 사용하는 팀에서는 토큰의 만료시간을 정해야 한다. 이는 다양한 플랫폼에 따른 전략들이 다를 수 있는데, 웹의 경우 탈취의 위험이 높기 때문에 access token과 refresh token을 빠르게 만료시키는 팀도 있으며, 앱의 경우에는 웹에 비해 탈취 가능성이 낮기 때문에 refresh token의 경우에는 인증 로직의 단순화를 위해 만료 시간을 사용하지 않는 경우도 있다. 또, 어떤 서비스의 경우에는 refresh token이 아예 없고 만료 시간이 없는 access token을 발급하기도 한다. refresh token을 배부하는 것은 optional이기 때문에 팀의 선택에 따라 다를 수 있다. 하지만, 서비스를 운영하는 입장에서 보안 이슈도 생각하여야 하고 유저의 편의성도 생각을 해야할 때, 어느정도 중도를 선택하는 방법은 무엇일까? 이러한 발행 전략으로 Sliding Session이라는 방법이 있다.

토큰 발행 전략 / Sliding Sessions

슬라이딩 세션은 일정 기간 이후에 만료가 되어지는 세션들을 의미한다. 이러한 슬라이딩 세션은 access token과 refresh token들을 사용하면 쉽게 구현될 수 있다. 유저가 로그인을 통하여 새로운 엑세스 토큰이 발행되고, 그 이후에 유저의 엑세스 토큰이 만료가 된다면, 그 세션은 비활성화가 되고, 새로운 토큰이 필요해진다. 이때, 이 토큰이 refresh token을 통하거나 또는 새로운 인증 과정을 거쳐서 access token을 얻거나 하는 방식은 개발 팀의 요구사항에 따라 정의될 수 있다. 이처럼 어떤 세션이 무조건 재 로그인을 통해서 access token을 받는 방식이 아니라, access token이 만료가 되더라도 로그인이 아닌 방식을 통해서 새로운 access token을 다시 발행을 받으면서 만료 시간을 아래 그림처럼 계속 늘려나갈 수 있는 방식을 슬라이딩 세션, 문자 그대로 움직이는 세션이다.

token-flow

구현 방법 - Access Token, Refresh Token With Sliding Session

이는 access token이 만료가 되어 refresh token으로 access token을 새로 발급 받을 때, refresh token은 한번만 사용되고 즉시 폐기 처리가 되고, 새로운 refresh token을 함께 발행해주는 방법이다. 이 방법을 사용하게 되면, 슬라이딩 세션을 지키면서 만약 유저가 refresh token의 만료 기간내에 접속을 하지 않을 경우에는 재 로그인을 하게 함으로써 보안성은 지키고, 자주 사용하는 유저의 경우에는 토큰이 만료될 때마다 새로운 access token과 refresh token을 함께 받아오기 때문에 로그아웃이 되는 문제를 보완해줘서 유저의 편의성도 지킬 수 있다. 다만, 만약 이 refresh token이 탈취가 되었다면, 계속적으로 새로운 토큰을 받아 갈 수 있기 때문에 보안 문제가 발생할 수 있다. 이러한 부분은 ‘비밀번호 변경’ 등을 통해 refresh token을 강제로 만료시켜 버리는 방법을 통해 해결할 수도 있다.

결론

REST API는 stateless이기 때문에 매 요청마다 인증이 필요하다. 하지만, 만약 이 관리를 서버사이드에서 처리하게 된다면, I/O가 증가되게 되고, 이는 레이턴시의 증가를 의미한다. 이러한 REST API 방식에서 JWT 토큰 방식은 토큰 자체가 마치 하나의 사원증과 같은 역할을 하기 때문에 서버 리소스를 사용하지 않아서 레이턴시를 줄여줄 수 있다는 장점이 있다. 단, 위에서 작성했듯이 클라이언트 사이드에서 관리하기 때문에 토큰 탈취에 대한 문제가 있고, 이를 해결하기 위한 패턴으로 Refresh token과 슬라이딩 세션 전략이 있지만, 토큰은 항상 탈취 될 수 있다는 점을 고려하여 볼 때, 서비스의 보안 중요성에 따라 적절한 방식을 채택하는 것이 좋을 것이다.

Post Directory