[JWT] Json Web Token
포스트
취소

[JWT] Json Web Token

JWT?

JWT(Json Web Token)는 Json 구조로 인증/인가에 필요한 데이터를 담고 서명하는 방식으로 토큰을 만들어 인증/인가를 진행하는 인증 방식입니다. 토큰에 필요한 정보를 담고 있기 때문에 서버에서 Stateless하게 처리가 가능하다는 장점이 있습니다.

프로세스

JWT의 발급-인증 과정에 대해 살펴보겠습니다.

ln1

발급 과정은 다음과 같습니다.

  1. 클라이언트가 로그인 요청을 보내면, 서버는 로그인 정보를 바탕으로 어떤 유저가 로그인 했는지 찾습니다.
  2. 서버에서 인증/인가 처리에 필요한 데이터를 담아 Json 형식으로 만들고, 서버에서 만들었다는것을 증명하기 위해 서명하여 토큰을 만듭니다.
  3. 클라이언트에게 JWT를 전달합니다.

JWT를 이용한 요청은 다음과 같습니다.

  1. 인증/인가가 필요한 api를 요청할때, 헤더에 발급받은 JWT을 포함시켜 보냅니다.
  2. 서버는 전달받은 JWT를 꺼내 맞는 서명인지, 어떤 사용자가 보낸 요청인지 등 여러 검사를 거쳐 요청자를 검증합니다.
  3. 조건에 맞으면 요청을 처리하고 결과를 전달합니다.

위 프로세스를 보면 알겠지만, 생성시에 JWT에 필요한 정보를 넣어두기 때문에 인증/인가 처리시 유저의 정보를 찾기 위해 서버에서 별도로 저장소를 찾아볼 필요가 없습니다. 그래서 MSA와 같이 여러 서버가 기능단위로 존재하는 아키텍쳐에서 Stateless하게 처리가 가능하다는 장점도 있습니다.
다만, JWT 자체가 탈취당하면 서버는 이를 알 길이 없기때문에 큰 보안 허점이 생기게 된다는 단점이 있습니다. 이를 해결하기 위한 방법은 뒤에서 마저 설명하겠습니다.

구조

JWT 구조는 다음과 같습니다.

ln1

Header, Payload, Signature를 각각 구성하고 Base64 방식으로 인코딩하여 각 부분을 .으로 이어 구성합니다.

1
2
3
4
{
    "alg": "RS256",
    "typ": "JWT"
}

Header에는 JWT에 사용된 서명 알고리즘을 기록합니다. 서버에서 JWT를 받고 헤더에서 어떤 알고리즘으로 서명됐는지 알아내어 Sginiture를 검증하여 우리 서버에서 생성한게 맞는지 검사합니다.

Payload

1
2
3
4
5
6
7
8
{
  "iss": "https://auth.example.com",
  "sub": "user_123",
  "aud": "orders-api",
  "exp": 1758700000,
  "iat": 1758696400,
  "scope": "orders:read"
}

Payload는 JWT 생성 시간, 유효 기간, 인증/인가에 필요한 사용자 정보 등 실제 구성 내용이 담기는 부분입니다. Payload의 구성 요소를 claim이라고 하는데, claim은 iss, sub, exp등 많은 사용자가 공통적으로 사용하는 규칙이 있습니다. 필요에 따라 원하는 claim울 추가하기도 합니다. 어떤 claim들이 있는지 궁금하면 이 페이지를 참고하시면 됩니다.
단, Payload는 Base64로 단순히 인코딩되어 JWT로 구성성되기 때문에, 민감한 정보를 절대 그대로 담으면 안됩니다. 그래서 이 단점을 보완하기 위해 토큰 자체를 암호화 하는 방식인 JWE 방식이 있습니다. JWE는 JWT를 구현하는 한 방식으로, 여기서는 자세히 다루지 않겠습니다.

Signature

Signature는 Base64(Header) + . + Base64(Payload) 문자열을 지정된 알고리즘으로 서명하여 구성합니다. 예를들어, 서버에서 고유의 키를 가지고 RS256로 서명하여 구성하고 전달한 후, 다시 받으면 같은 방식으로 서명하여 Signature와 동일한 값이 나오는지 검증합니다. 서버에 직접적으로 침투하지 않는 이상 서명에 사용된 고유 키는 알 수 없기 때문에 검증이 가능합니다.

보안

프로세스와 구조를 살펴보니 JWT의 단점들이 보입니다. 일단 토큰으로만 인증/인가를 처리하는 특성상 한번 만들고 탈취당하면 무한으로 탈탈 털릴 수 있다는점이 가장 큽니다. 그래서 생각한 방법이 만료 시간을 추가하여 일정 기간 안에만 해당 토큰으로 인증/인가 처리가 가능하게 만드는 것 입니다.

exp (만료시간)

위에 Payload 예제를 보면 exp가 있습니다. 이는 expired(만료시간)으로, 해당 JWT는 exp 시간까지만 사용이 가능하다고 명시합니다. exp를 발급 시점의 10분~20분 뒤로 설정해두면, 당장 탈취당하더라도 exp 시간 동안만 털리게 됩니다.
다만, 이번에는 사용자 경험 측면에서 문제가 발생합니다. 사용자는 서비스를 계속 이용하고 싶은데, 강제로 20분 뒤에는 토큰 인증에 실패하며 로그아웃 당하는 경험을 하게됩니다. 그렇다고 시간을 2시간, 24시간 이렇게 늘려버리면 exp를 추가한 이유가 사라집니다.
그래서 고안된게 Refresh Token입니다.

Refresh Token

Refresh Token(RT)은 이름과 같이 인증/인가에 사용되는 토큰을 재발급하기 위한 토큰입니다. RT 역시 JWT의 구성을 따릅니다. 보통 이 구조에서는 인증/인가에 사용되는 토큰을 Access Token(AT)라고 부릅니다. AT-RT 구조의 프로세를 정리하면 다음과 같습니다.

발급

ln1

발급은 기존 방식과 동일합니다. 차이점은 AT와 RT 두 토큰을 발급한다는 것 말고는 없습니다.
이떼 AT는 짧게는 20분, 길게는 2시간 등 유효기간을 짧게, RT는 2주, 30일 등 유효기간을 길게 설정합니다.

인증/인가

ln1

인증/인가가 필요한 api 요청시 기존 방식대로 헤더에 AT를 포함시켜 요청합니다. 서버는 AT를 검증하고, 유효기간이 만료됐으면 Refresh가 필요하다는 오류를 보냅니다. 아직 유효한 AT라면 api 처리 결과를 보냅니다.

Refresh

ln1

Refresh 요청에는 RT를 보냅니다. 서버는 RT도 마찬가지로 검증하여 유효기간이 남았으면 새 AT를 생성해 보내줍니다. 만약 RT마저도 만료되었으면 클라이언트에서 로그아웃 처리 하라는 오류를 보냅니다.


이처럼 AT-RT구조를 이용해 그냥 JWT를 사용할때보다 안전하게, 긴 텀동안 로그인이 유지되도록 만들 수 있습니다. 그러나 이 방식 역시 완벽하지 않습니다.

문제점

우선 AT가 인증 시간이 줄었다지만 여전히 탈취시 만료까지 언제든지 개인정보가 탈탈 털릴 수 있습니다. AT가 탈취 당한 후 즉시 새 AT를 발급받아도 탈취한 AT는 만료될 때 까지 사용이 가능합니다. 그리고, 어쨌거나 RT도 만료되면 로그아웃 처리가 되어야합니다.
이러한 문제점을 해결하기 위해, RT를 서버에 저장해두고 AT 재발급마다 새로 생성해 저장하는 Refresh Token Rotation 방식, AT에 고유 키를 만들어 AT 키를 저장해두고 요청시 들어온 AT가 현재 키와 맞지 않으면 인증 실패 처리하는 방식 등 다양한 보완 방식이 존재합니다.
물론, 서버에 뭔가 저장하는 순간부터 JWT의 Stateless한 장점은 옅어집니다. 보안성이 높을수록 Stateless의 장점은 낮아지고, Stateless 장점을 활용할수록 보안성이 낮아진다고 생각할 수 있을것 같습니다. 자신의 서비스에 필요한 보안 정도를 정하고 그때그때 맞게 구현해서 사용하면 됩니다.
저는 DPop(Demonstration of Proof-of-Possession) 방식으로 JWT의 단점을 보완하여 사용중입니다. 다음 글에서 DPop에 대해 자세히 알아보겠습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

[ML] RNN/LSTM

[DPop] Demonstration of Proof-of-Possession