JWT(Json Web Token)
일반적으로 클라이언트와 서버 사이에서 통신할 때 정보를 비밀리에 전달하거나 권한을 부여하고 인증 하기위해 사용하는 토큰이다.
이름에서 알 수 있듯이 웹상에서 정보를 Json 형태로 주고 받기 위해 표준규약에 따라 생성한 암호화된 토큰 형태로 제작되며 따로 디코딩 하지 않으면 해석할 수 없는 String형태로 되어있다.
토큰 인증 방식의 한 가지 종류로서, 클라이언트가 토큰을 가지고 있다가 API 요청을 할 때 JWT를 전달하여 인증 & 인가를 진행하게 된다.
JWT의 구성요소
JWT는 헤더(Header), 페이로드(Payload), 서명(Signature) 세 파트로 구성되는데 Json으로 포맷된 각 부분은 Base64로 인코딩되어 표현되며, 각각의 부분을 이어주기 위해 구분자로 '.'을 사용한다.
1. Header
토큰의 헤더는 typ와 alg 두가지 정보로 구성된다.
typ : 토큰의 타입을 지정
alg : 알고리즘 방식을 지정하며, 서명 및 토큰 검증에 사용 ex)HS256 등 (헤더가 아닌 Signature를 해싱하기 위한 알고리즘을 지정하는 것)
{
"alg": "HS256",
"typ": JWT
}
2. PayLoad
페이로드에는 토큰에서 사용할 정보들의 조각인 클레임(Claim)이 담겨 있다.
클레임은 총 3가지로 나누어지며, Json 형태로 다수의 정보를 담을 수 있다.
1) 등록된 클레임(Registered Claim)
등록된 클레임은 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터들로, 모두 선택적으로 작성이 가능하지만 강력하게 사용이 권고되는 것들이다.
iss : 이 데이터의 발행자
iat : 이 데이터가 발행된 시간
exp : 이 데이터가 만료된 사간
sub : 토큰의 제목
aud : 토큰의 대상
nbf : 토큰이 처리되지 않아야 할 시점, 이 시간이 지나기 전엔 토큰이 처리되지 않음
jti : 토큰의 고유 식별자
2) 공개 클레임(Public Claim)
사용자 정의 클레임으로 공개용 정보를 전달하기 위해 사용한다. 사용자 마음대로 쓸 수 있으나 충돌 방지를 위해 URI 포맷을 이용해 키를 정한다.
3) 비공개 클레임(Private Claim)
마찬가지로 사용자 정의 클레임으로 비공개용 정보를 전달하기 위해 사용한다. 통신을 주고받는 당사자들끼리 협의해서 임의로 키와 값을 정할 수 있다.
3. Signature
서명은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드다.
서명은 헤더와 페이로드, 그리고 비밀키를 기반으로 생성되며 해당 토큰이 변조된 것이 아님을 확인하기 위한 매커니즘이다.
서명은 위에서 만든 헤더와 페이로드의 값을 각각 Base64로 인코딩하고, 인코딩한 값을 비밀키를 이용해 헤더에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 Base64로 인코딩하여 생성한다.
JWT의 동작 원리
- 사용자가 id와 password를 입력하여 로그인 요청을 한다.
- 서버는 회원DB에 들어가 있는 사용자인지 확인한다.
- 확인이 되면 서버는 로그인 요청을 확인한 후, secret key를 통해 토큰을 발급한다.
- 이것을 클라이언트에 전달한다.
- 서비스 요청과 권한을 확인하기 위해서 헤더에 JWT를 담아 요청을 한다.
- 데이터를 확인하고 JWT에서 사용자 정보를 확인한다.
- 클라이언트 요청에 대한 응답을 전달한다.
JWT의 단점 및 고려해야 할 점
- Self-Contained : 사용자 인증에 필요한 모든 정보를 토큰 자체에 정보를 담고 있기 때문에 별도의 인증 저장소가 필요없고 이 덕분에 트래픽에 대한 부담이 낮다는 장점 때문에 Claim 기반의 JWT가 선호되는 것은 맞지만 이 것은 토큰이 탈취 당할 경우 보안적 측면에서 굉장한 리스크를 갖게 되므로 양날의 검이라 할 수 있으며, 탈취 시나리오에 대해서 염두에 두어야 한다.
- 토큰 길이: 토큰의 페이로드에 클레임들을 저장하기 때문에 정보가 많아질수록 토큰의 길이가 늘어나 네트워크에 부하를 줄 수 있다.
- Payload 인코딩 : 페이로드 자체는 암호화되지 않아서 중간에 페이로드를 탈취하여 디코딩하면 데이터를 조회해 볼수 있으므로 따로 암호화를 하거나 Payload에 중요한 정보를 넣지 않아야 한다.
- Stateless: JWT는 상태를 저장하지 않기 때문에 한 번 만들어지면 제어가 불가능하다. 즉, 토큰을 임의로 삭제하는 것이 불가능하기 때문에 토큰 만료시간을 꼭 넣어주어햐 한다.
Access Token과 Refresh Token
JWT의 단점들 중 Stateless 즉 토큰의 제어가 불가능한 점을 보완하기 위해 사용하는 방법이 Access Token과 Refresh Token을 같이 활용하는 방법이다.
로그인을 하게 되면 Access Token과 Refresh Token을 동시에 발급하며 Access Token을 통해 인가를 진행한다. 그리고 인가를 진행하는 Access Token은 만료시간을 짧게 가져가서 혹시라도 Access Token이 탈취당하더라도 이 Token을 이용해 서버에 접근하는 일을 방지할 수 있다.
그렇다면 클라이언트가 서버에 접근하기 위해선 Access Token이 필요하고 Access Token이 없어질 때마다 새로 Access Token을 발급해주어야 하는데 만료시간을 짧게 했기 때문에 Access Token 발급을 위해 계속해서 로그인을 해야한다면 굉장히 불편한 사용경험이 될 수 밖에 없다.
이 때 Access Token과 같이 발급했던 Refresh Token을 사용하게 되는데 Access Token이 만료됐을 때 만료기간을 상대적으로 길게 설정한 Refresh Token이 존재한다면 추가적인 로그인 과정 없이 Access Token을 재발급하도록 설계해주면 된다.
RTR(Refresh Token Rotation)
앞서 Access Token과 Refresh Token의 개념과 작동 원리에 대해 알아보았는데 이 Access Token과 Refresh Token을 같이 활용하는 방식의 가장 큰 문제점이 하나 있다. 바로 비교적 만료기간을 길게 설정한 Refresh Token이 탈취당하면 이 탈취당한 Refresh Token을 활용해 Access Token을 계속해서 재발급 받으며 서버에 접근할 수 있게 된다는 점이다.
추가로 JWT 토큰은 서버가 관리하지 않는 상태(Stateless)이기 때문에 탈취당했는지 서버는 알 수가 없고 만료시간이 될 때까지 기다리는 것 외에는 방법이 없다는 것이다.
이것을 방지하기 위해 사용하는 것이 RTR 기법이다.
RTR 기법을 간단하게 설명하자면 Refresh Token을 일회용으로 사용하고 계속해서 새로운 Refresh Token으로 바꿔주는 기법이다.
RTR 작동 원리
- Access Token 1과 Refresh Token 1을 얻는다.
- Refresh Token 1을 사용하여 Access Token 2와 Refresh Token 2를 얻는다.
- Refresh Token 2를 사용하여 Access Token 3와 Refresh Token 3를 얻는다.
- 이 과정을 계속 반복한다.
그래서 내가 보안을 위해 사용한 방법은?
- 토큰이 필요하지 않은 API endpoint를 제외하고는 요청에 Access Token을 담아야만 접근이 가능하도록 했다.
- 토큰은 발급과 동시에 Response Cookie에 설정 후 응답.(httpOnly 속성을 부여해서 프론트에서 접근하지 못하도록 함 -> Xss 공격 방지)
- Refresh Token은 발급할 때 "email - Refresh Token" 쌍으로 Redis에 보관.
- Access Token이 만료된 상태로 API 요청을 보낸 경우 쿠키에서 추출한 Refresh Token의 유효성을 검사해 Access Token을 재발급 하고 RTR 기법을 활용해 Refresh Token 재발급 및 Redis에 저장된 Refresh Token도 업데이트 해준다.
- 만약 제 3자가 탈취한 Refresh Token으로 만료된 Access Token을 들고 새로 요청을 한다면 Refresh Token이 Redis에 저장된 Refresh Token과 일치하지 않아서 새로 로그인을 해야한다.
- 이러한 방법은 JWT의 특징인 Stateless 방식과 어긋나있긴 하지만 서버에 요청하는 횟수는 Session보다는 적다는 점 그리고 Session에 비해 보안적으로도 안전하기 때문에 결과적으로 채택