JWT, 누구냐 넌

mynghn·2022년 11월 20일
0

JWT, 알고 쓰자.

목록 보기
2/2
post-thumbnail

🪙 JWT란?

RFC 7519에 따르면,

JWT는 HTTP Authorization 헤더URI의 쿼리 파라미터 같은 공간 집약적인 환경을 위해 컴팩트한 형태로 고안된 클레임(claim)들을 표현하는 하나의 형식이다.

claim이란 JWT을 발급한 측에서 제공하는 정보를 부르는 용어다. JWT를 통해 유저 인증을 한다면 해당 유저의 ID 같은 정보가 되겠다.

그 안에 담기는 정보가 모두 JSON 오브젝트의 형태라서 JSON Web Token이다.

어쨌든 JWT는 정보를 컴팩트하게 표현해 네트워크를 통해 두 당사자 사이에서 주고 받는 데 그 목적이 있다.

그리고 주고 받는 정보의 보안을 유지하기 위한 처리 방식에 따라 실제 구현이 달라지는데,

보통의 JWT는 JWS 방식의 signed token을 말하고 이 글에서 살펴볼 JWT 구조도 JWS 기준으로 설명한다.

signed token vs. encrypted token

JWT는 JWS 방식 아래에선 signed token이 되고 JWE 방식 아래에선 encrypted token이 된다.

signed token은 디지털 서명이 된 토큰이고, 토큰을 손에 넣게 되는 자는 누구나 그 안의 내용을 볼 수 있다. 대신 서명을 통해 해당 내용이 위조되었는지를 검증할 수 있다.

반면 encrypted token은 내용이 암호화 처리가 된 토큰이다. 따라서 토큰을 손에 넣게 되어도 복호화를 할 수 없는 자는 그 안의 내용을 알 수 없다.

🏛️ JWT의 구조

JWT의 정체는 단순하게 봤을 때, . 구분자에 의해 세 파트로 나눠진 문자열 이다.

세 파트는 순서대로 헤더, 본문(payload), 서명(signature)이고,

모든 파트가 다 Base64URL 인코딩을 거치기 때문에 결과적으로 모두 문자열일 수밖에 없다.

Base64/Base64URL 인코딩

Base64는 임의의 8비트 단위 바이너리 데이터 시퀀스(실행 파일 등)를 6비트 단위 문자 4개씩의 문자열로 인코딩하는 방식. 6비트가 한 단위가 되기 때문에 26=642^6=64개의 문자로 모든 표현을 해서 64진법 Base64다.

기본 Base64 인코딩에 사용되는 63, 64번 문자 +, /가 URL에서 특수한 기능을 수행하는 예약 문자들이기 때문에 만약 63, 64번 문자가 있는 기본 Base64 인코딩된 문자열을 URL에 포함하고 싶다면 URL 인코딩을 한 번 더 거쳐야 한다. 그래서 이를 피하기 위해 63, 64번 문자를 각각 -, _ 문자로 대체한 방식이 Base64URL이다.

그리고 앞서 언급한 것처럼 JWT는 URL을 통해 전달되는 경우를 고려해 디자인되었기 때문에 url-safe한 Base64URL 인코딩 방식을 사용하는 것.

헤더와 본문은 인코딩 전 원래는 JSON 오브젝트고,

서명은 암호화 알고리즘에 의해 해싱된 값이라 인코딩 전에도 문자열이다.

그리고 JWS 기준,
JWT 안의 내용을 보려면 Base64URL 인코딩된 각 파트를 디코딩만 하면 된다.

서명은 어차피 암호화 후 해시된 값이기 때문에 디코딩해도 볼 게 없겠지만,
헤더와 본문은 디코딩만 하면 원래의 JSON 오브젝트를 그대로 확인할 수 있다.

헤더

헤더와 본문은 둘 다 여러 개의 key-value 쌍이 담긴 JSON 오브젝트가 그 정체다.

먼저 헤더는 다음과 같이 두 쌍의 key-value로 구성한다.

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg: 디지털 서명에 사용할 암호화 & 해싱 알고리즘의 종류. HS256은 이 토큰에 HMAC-SHA256 방식이 사용됐음을 의미. 보다 자세한 이야기는 아래 섹션에서 부연.
  • typ: 해당 토큰의 타입. 예외적인 케이스가 아니면 JWT로 두면 된다.

이외에도 헤더에 넣을 수 있는 데이터 종류가 더 있지만, 일반적인 유저 인증 시나리오에선 위의 두 가지 데이터만으로 헤더를 구성하면 된다.

이제 JSON 오브젝트를 그대로 Base64URL 인코딩하면 JWT의 첫 파트에 들어갈 문자열이 마련된다.

본문(Payload)

본문은 JWT의 주인공인 클레임들이 들어가는 곳이다.

헤더와 같이 결국 정체는 key-value 쌍을 모아놓은 JSON 오브젝트고,
여기서 key-value 쌍 하나하나가 클레임의 이름과 값이 된다.

본문에서 다루는 클레임에는 세 가지 유형이 있다.

1. Registered claims

가장 많이 사용하는 클레임 유형으로,
이름이 이미 정의되어 있고 그걸 따라서 사용해야 한다.

모든 JWT 사용자들을 위해 표준화되어 제공되고 있는 클레임 유형들이고,
모두 IANA JSON Web Token Claims registry 표준에 등록되어 있다.

일종의 JWT 빌트인 혹은 예약어 클레임들이라고 이해하면 되겠다.

아래는 몇 가지 예시.

  • iss: JWT 발급 주체
  • sub: 본문에 담긴 클레임들이 설명하는 주제
  • aud: JWT를 수신할 대상. JWT를 처리하는 당사자는 모두 명시되어야 한다.
  • exp: JWT의 만료 시간
  • iat: JWT의 발급 시점
  • jti: JWT의 ID

컴팩트한 JWT의 구성 원칙에 의해 registered 클레임은 이름이 모두 세 글자다.

2. Public claims

이미 등록된 클레임들에 더해 사용자들이 직접 공개적으로 추가하는 클레임.

IANA registry에 등록을 새로 하거나, 등록된 클레임들과의 충돌을 예방하는 Collision-Resistant한 이름을 갖고 있어야 한다.

일종의 오픈소스 기여 같은 방식으로 나는 이해했다.

collision-resistant한 이름 체계의 경우 URL 형식으로 이름을 짓는 방식이 있다.

{
    "https://example.com/jwt_claims/public_claim": "public_claim"
}

3. Private claims

JWT를 통해 통신하는 양 측의 합의 하에 커스텀하게 추가하는 클레임들.

{
    "user_id": 1
}

공개 클레임과 달리 등록된 클레임들과의 충돌은 두 당사자 사이에서 잘 해결하면 된다.


이제 세 가지 유형의 클레임들 중 필요한 것들을 조합해서 아래와 같은 JSON 오브젝트로 본문을 구성할 수 있고,

{
  "token_type": "access",
  "exp": 1668858265,
  "iat": 1668857965,
  "jti": "fa6bb48119c74bad902799d0141e8072",
  "user_id": 1
}

인코딩까지 해주면 두 번째 파트도 완성.

서명(Signature)

JWS 기준으로,
JWT가 제공하는 보안은 디지털 서명을 통해 중간 변조 여부를 탐지하는 것이고, 이를 달성하기 위해 암호화 이후 해싱(hashing) 과정의 결과물을 서명 파트로 JWT에 포함한다.

해싱(hashing) vs. 암호화(encryption)

해싱은 임의의 길이를 가진 데이터를 특정 해시 함수를 거쳐 고정된 길이의 데이터로 매핑하는 것을 말한다. 모든 값은 고유한 해시 값을 갖고, 해시 값은 원래의 값으로 쉽게 복원할 수 없다.

암호화는 특정 암호화 알고리즘에 임의의 평문(plaintext) 데이터와 암호화 키(key)를 함께 넣어 암호문(ciphertext) 데이터로 만드는 것을 말한다. 암호문은 평문으로 다시 복호화할 수 있지만 복호화 키가 있어야 가능하다.

JWT 서명을 만드는 방법은,

  • 앞서 인코딩까지 완료한 두 파트에 암호화 키를 섞어 암호문을 만들고
  • 이를 다시 해싱한 뒤
  • 마지막으로 해시값을 Base64URL 인코딩하면 된다.

만약 암호화 이후 해싱 알고리즘으로 HS256(HMAC-SHA256) 방식을 사용한다면 다음과 같은 과정이다.

base64UrlEncode(
  sha256Hash(
    hmacEncrypt(
      base64UrlEncode(header) + "." + base64UrlEncode(payload),
      secret
    )
  )
)

JWT의 중간 변조 여부를 검증하려면,
암호화 키가 있는 수신자헤더에 명시된 알고리즘을 사용해 인코딩된 헤더+본문을 다시 암호화 후 해싱해 수신한 서명의 내용과 같은지를 비교하면 된다.

이론적으로는 데이터의 변조를 탐지하는 것은 암호화만을 거쳐도 가능하다.
(변조 여부를 숨기려면 중간 탈취자에게 암호화 키가 있어야 하기 때문)

하지만 이후 해싱 과정이 없다면,
검증 과정에서 매번 복잡하고 큰 구조의 헤더와 본문을 대조하는 작업을 거쳐야 한다. 해싱의 역할은 고정된 길이의 해시값을 대조하는 것으로 이 과정을 대체해서 검증을 효율적으로 수행하기 위한 것으로 보인다.


이쯤 하면 JWT의 정체에 대한 파악이 끝났다.

다음 글에선 이제 JWT의 유즈 케이스를 유저 인증으로 삼았을 때 고려해야 할 것들에 대해 다뤄보겠다.

0개의 댓글