JWT - Json Web Token
이번 포스팅에서는 Json Web Token, 줄여서 흔히 JWT라고 불리는 사용자 인증/인가 수단 대해서 알아보도록 하겠습니다.
JWT 란?
JWT(Json Web Token)는 말그대로 웹에서 사용되는 JSON 형식의 토큰에 대한 표준 규격인데요. RFC 7519로 정의되어 있으며, 토큰을 어떤 구조로 만들고 어떻게 서명하는지, 그리고 안에 담을 수 있는 표준 claim에는 어떤 것들이 있는지를 규정합니다. 주로 사용자의 인증(authentication) 또는 인가(authorization) 정보를 서버와 클라이언트 간에 안전하게 주고 받기 위해서 사용됩니다.
JWT 토큰 웹에서 보통 Authorization HTTP 헤더를 Bearer <토큰>의 형태로 설정하여 클라이언트에서 서버로 전송되며,
서버에서는 토큰에 포함되어 있는 서명(signature) 정보를 통해서 위변조 여부를 빠르게 검증할 수 있게 됩니다.
이 서명이 어떻게 만들어지는지 더 깊이 들여다보고 싶다면 해시 함수와 비대칭키 암호화 글을 함께 보시면 좋습니다.
JWT 토큰은 Base64로 인코딩이 되어 있어서 육안으로 보면 eyJ로 시작하는 아주 긴 문자열인데요.
온라인 디버거를 통해서 어렵지 않게 실제로 저장되어 있는 내용을 JSON 형태로 디코딩하여 확인해볼 수 있습니다.
JWT 구조
하나의 JWT 토큰은 헤더(header)와 페이로드(payload), 서명(signature) 이렇게 세 부분으로 이루어지며 각 구역이 . 기호로 구분됩니다.
<헤더>.<페이로드>.<서명>
첫 번째 부분인 헤더(header)에는 토큰의 유형과 서명 알고리즘에 명시되고, 중간 부분인 페이로드(payload)에는 소위 claim이라고도 불리는 사용자의 인증/인가 정보가 담기는데요. 마지막 부분인 서명(signature)에는 헤더와 페이로드가 비밀키로 서명되어 저장됩니다.
JWT 토큰은 네트워크로 전송되야 하기 때문에 공간을 적게 차지하는 것이 유리한데요. 그래서 독특하게도 JSON 형식으로 데이터를 저장할 때 키(key)를 3글자로 줄이는 관행이 있습니다.
아래는 전형적인 JWT 토큰의 디코딩 결과입니다.
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"iat": 1516239022
}
JWT의 키는 어느 구역에 들어가느냐에 따라 정의하는 표준이 다른데요. 헤더 파라미터는 서명을 다루는 RFC 7515(JWS)가, 페이로드 claim은 RFC 7519(JWT)가 정합니다. 자주 쓰이는 키를 구역별로 정리하면 다음과 같습니다.
| 구역 | 키 | 의미 | 정의 |
|---|---|---|---|
| 헤더 | typ | 토큰 유형(type) | RFC 7515 (JWS) |
| 헤더 | alg | 서명 알고리즘(algorithm) | RFC 7515 (JWS) |
| 헤더 | kid | 서명에 쓴 키 식별자(key ID) | RFC 7515 (JWS) |
| 페이로드 | iss | 토큰 발급처(issuer) | RFC 7519 (JWT) |
| 페이로드 | sub | 사용자(subject) | RFC 7519 (JWT) |
| 페이로드 | aud | 토큰을 받을 대상(audience) | RFC 7519 (JWT) |
| 페이로드 | exp | 만료 시각(expiration time) | RFC 7519 (JWT) |
| 페이로드 | nbf | 활성 시작 시각(not before) | RFC 7519 (JWT) |
| 페이로드 | iat | 발급 시각(issued at) | RFC 7519 (JWT) |
| 페이로드 | jti | 토큰 고유 식별자(JWT ID) | RFC 7519 (JWT) |
헤더의 typ·alg·kid가 “이 토큰을 어떻게 검증하는가”를 다룬다면, 페이로드 claim은 “이 토큰이 무엇을 주장하는가”를 담습니다.
특히 kid는 서명에 쓴 키가 무엇인지 가리켜, 검증하는 쪽이 여러 공개 키 중 맞는 것을 골라낼 수 있게 해주는데요. 이 키 선택과 교체(rotation) 메커니즘은 JWK와 JWKS 이해하기에서 자세히 다룹니다.
이렇게 표준이 갈리는 건 JWT가 JOSE(JavaScript Object Signing and Encryption) 라는 표준 가족 위에 세워졌기 때문인데요. JOSE는 서명을 담당하는 JWS, 암호화를 담당하는 JWE, 키 형식을 정의하는 JWK, 알고리즘을 정의하는 JWA로 이루어진 묶음입니다. 전체 그림은 JOSE 한눈에 보기에 정리해 두었어요. JWT는 claim을 JWS로 서명해 만든 토큰이라, 그 헤더가 곧 JWS의 헤더인 셈이죠.
한 가지 기억해 둘 점은 RFC 7519가 이름을 정해둔 이 등록된 claim(registered claim)들이 뜻만 약속됐을 뿐 어느 것도 필수가 아니라는 것입니다.
무엇을 반드시 넣을지는 JWT를 가져다 쓰는 쪽에서 정하는데요. 예를 들어 OAuth 액세스 토큰이라면 RFC 9068이 이 가운데 여럿을 필수로 못 박고 client_id 같은 claim을 더합니다. 바로 뒤에서 자세히 살펴보겠습니다.
JWT를 통한 인증/인가
JWT는 실무에서 OAuth나 OIDC 프로토콜과 함께 API의 인증이나 인가를 위해서 주로 사용이되는데요.
보통 클라이언트가 어떤 서비스의 인가 서버를 통해 로그인에 성공하면 JWT 토큰을 획득할 수 있는데요. 그러면 클라이언트는 해당 서비스의 API를 호출할 때 JWT 토큰을 보내서 원하는 자원에 접근하거나 허용된 작업을 수행할 수 있게됩니다.
OAuth나 OIDC에 대한 자세한 내용은 아래 관련 포스팅을 참고 바라겠습니다.
OAuth 액세스 토큰의 표준, RFC 9068
앞에서 봤듯이 RFC 7519는 JWT라는 그릇의 형식만 정할 뿐, 그 안에 무엇을 담아야 하는지는 열어둡니다. 그러다 보니 같은 JWT라도 로그인한 사용자를 나타내는 ID 토큰인지, API 접근 권한을 나타내는 액세스 토큰(access token)인지 형식만 봐서는 구분하기 어려운 문제가 있었는데요.
이 문제를 풀기 위해 등장한 것이 RFC 9068, 제목 그대로 “OAuth 2.0 액세스 토큰을 위한 JWT 프로필”을 정의한 표준입니다. 여기서 프로필(profile)이란 RFC 7519라는 범용 규격을 ‘OAuth 액세스 토큰’이라는 한 가지 용도에 맞게 더 좁고 엄격하게 다듬은 규칙을 말하는데요. 그래서 RFC 9068 토큰은 모두 올바른 RFC 7519 JWT이지만, 그 반대는 성립하지 않습니다.
구체적으로는 크게 두 가지를 규정합니다. 우선 헤더의 typ 값을 평범한 JWT 대신 at+jwt로 지정해서 이 토큰이 액세스 토큰임을 분명히 합니다.
ID 토큰은 보통 typ가 없거나 JWT라서 다른 JWT와 형식만으로는 구분되지 않는데요. 액세스 토큰에 at+jwt를 박아두면 자원 서버가 그 값을 보고 거를 수 있어서, 실수로(혹은 악의적으로) ID 토큰을 액세스 토큰처럼 갖다 쓰는 토큰 혼동(token confusion)을 막을 수 있습니다.
{
"alg": "RS256",
"typ": "at+jwt"
}
두 번째로, 액세스 토큰에 반드시 담아야 하는 claim을 정해두었습니다.
앞에서 RFC 7519에서는 선택이라고 했던 iss, exp, aud, sub, iat을 여기서는 모두 필수로 못 박고, 거기에 토큰을 발급받은 클라이언트를 식별하는 client_id와 토큰마다 고유한 식별자인 jti까지 반드시 넣도록 했는데요.
여기에 권한 범위를 나타내는 scope나 역할을 담는 roles 같은 인가 정보를 함께 실어 보냅니다.
{
"iss": "https://auth.example.com",
"sub": "1234567890",
"aud": "https://api.example.com",
"client_id": "my-client",
"iat": 1516239022,
"exp": 1516242622,
"jti": "a1b2c3d4",
"scope": "read write"
}
이렇게 형식이 표준으로 정해져 있으면 자원 서버(Resource Server)는 매 요청마다 인가 서버에 토큰이 유효한지 물어볼 필요가 없는데요.
서명을 검증하고 aud에 자기 자신이 들어 있는지, exp가 지나지 않았는지만 확인하면 됩니다.
이 검증 흐름이 실제로 어떻게 쓰이는지는 OAuth 2.0 엔드포인트 글에서 더 자세히 다룹니다.
JWT의 장점
JWT가 등장하기 전에는 웹에서 쿠키(cookie)와 세션(session)을 이용한 사용자 인증을 구현하는 경우가 많았는데요. 그럼 JWT가 기존 방법 대비 어떤 강점이 있어서 이렇게 대세로 자리를 잡게 되었을까요?
아마도 가장 큰 이유는 확장성에 있을 것 같은데요. JWT는 토큰 자체에 사용자의 정보가 저장되어 있어있기 때문에 서버 입장에서 토큰을 검증만 해주면 됩니다.
반면에 쿠키와 세션을 사용할 때는 서버 단에 로그인한 모든 사용자의 세션을 DB나 캐시(cache)에 저장해놓고 쿠키로 넘어온 세션 ID로 사용자 데이터를 매번 조회해야만 하죠.
따라서 JWT를 사용할 때는 사용자가 늘어나더라도 사용자 인증을 위해서 추가로 투자해야하는 인프라 비용을 크게 절감할 수 있습니다.
뿐만 아니라 쿠키를 사용하지 않으므로 CORS 문제에서 자유로워진다는 것도 장점으로 여겨질 수 있겠습니다.
쿠키나 세션에 대한 자세한 내용은 아래 관련 포스팅을 참고 바라겠습니다.
JWT의 한계
위와 같은 JWT의 장점에도 불구하고 어느 정도 규모가 있는 서비스에서 사용자 인증 용도로 JWT를 사용하기에는 부족한 경우가 있는데요.
예를 들어, 현재 로그인된 사용자의 모든 장비들을 나열해주거나, 특정 장비에서 로그아웃을 허용하는 기능을 구현하려면 서버 단에 사용자 세션을 저장하지 않고는 어렵기 때문입니다.
JWT 사용 시 주의 사항
서명이 되어 있는 JWT 토큰 서버에서만 유효성을 검증할 수 있지만 그 안에 저장된 데이터는 누구나 쉽게 열람이 가능합니다. 따라서 민감한 사용자 정보를 JWT 토큰에 그대로 저장하게 되면 큰 보안 문제로 이어질 수 있어서 각별한 주의가 필요하겠습니다.
가급적 JWT 토큰에는 사용자를 식별할 수 있는 아이디 정도만 저장하는 것이 좋으며 해당 사용자에 대한 추가 정보가 필요한 경우에는 서버에서 사용자 DB를 조회하는 것이 안전할 것입니다. 불가피한 이유로 JWT 토큰에 민감한 사용자 정보를 저장해야한다면 반드시 암호화를 하여 JWT 토큰을 디코딩한 후에도 알아볼 수 없게 해야 할 것입니다.
마치며
이상으로 JWT에서 대해서 개념을 파악하는 수준으로 간단하게 알아보았습니다. 다음 포스팅에서는 자바스크립트로 어떻게 JWT 토큰을 발급하고 검증하는지에 대해서 알아보겠습니다.
JWT에 연관된 포스팅은 JWT 태그를 통해서 쉽게 만나보세요!
This work is licensed under
CC BY 4.0