JSON Web Token의 약어로서 토큰을 이용한 인증 메커니즘이다. 토큰이라는 용어가 의미하는 것은 임시로 사용하고 쉽게 버리고 재발급할 수 있다는 의미이며, 대중교통의 승차권과 같은 개념으로 생각하면 이해에 도움이 된다. 지하철을 이용할 때 출발지에서 승차권(토큰)을 구입해서 사용하고 목적지에 도착하면 폐기하면 된다. 지하철 월 정기권을 분실한다면 누군가가 주워서 한 달 동안 잘 써먹을 것이다. JWT도 마찬가지라서 분실을 대비해 유효기간이 너무 길지 않도록 만들어야 한다. 게다가 이 승차권에다가 나의 집 비밀번호를 적어두면 안 되는 것처럼 JWT에도 기밀정보를 저장하면 안 된다.
세션은 서버에서 수명주기를 관리해야 하고 서비스가 이중화되면 DB와 같은 저장소에 보관해야 하는 부담이 있다. 고가용성까지 생각하면 특정 서비스 노드에 장애가 발생했을 때 세션 정보를 클러스터링(서버 간 세션 데이터 공유)하는 것도 필요한 경우가 (드물지만) 존재한다. 이런 이유로 웹서비스 개발자들은 쿠키에 기밀정보를 담아서 인증을 대체하고자 하려는 경향이 있다. 아무래도 서버에 저장하지 않고도 클라이언트에서만 관리할 수 있다면 가볍지 않은가?
어쨌든 쿠키는 이런 목적을 위해서는 안전하지 않은 수단이기에, 인증 토큰이 유효한지를 서버가 저장없이 검증할 수 있는 메커니즘을 마련한 것이 JWT이다.
B64(헤더) . B64(페이로드) . 시그니처
여기서 B64는 base64 인코딩을 의미한다.
실제로는 한 줄로 되어 있다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
헤더와 페이로드는 각각 base64로 인코딩된 문자열이기 때문에 디코드 가능하다.
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
시그니처는 각각 인코드된 헤더와 페이로드를 점으로 이어붙여서 암호화한 값이다.
해시 알고리즘으로 HMACSHA*를 사용한다. (위 예제의 HS256이 이것의 약어이다.) 공유하는 암호(대칭키)를 이용해서 해시값을 구하고, 동일한 절차로 해시값을 만들어서 전달받은 기존 해시값과 비교하여 토큰의 변조 여부를 판정할 수 있다.
다음 알고리즘으로 서명을 만들 수 있다.
HMACSHA256(
B64(헤더) + "." + B64(페이로드),
암호)
대칭키를 외부에 공유해야 하므로 보안 상 부담이 있다. 대칭키가 유출되면 위변조가 가능해진다. 그러므로 별도의 보안 장치가 없는 한, 이 방식은 사용하지 않는 게 바람직하다.
암호화 알고리즘으로 RSASHA*나 ECDSASHA*, RSAPSSSHA*를 사용한다.
(256비트, 384비트, 512비트 길이의 키를 지정할 수 있기 때문에 이렇게 *로 표기했다.)
RSASHA256(
B64(헤더) + "." + B64(페이로드),
private key
)
비대칭키 방식을 이용하기 때문에 퍼블릭 키만 공개하면 되므로 대칭키 방식에 비해 상당히 안전하다.
API 서버가 Auth 서버나 DB에 의존하지 않는 점에 주목할 것. 이렇기 때문에 인증 과정은 오로지 Auth 서버에게 맡겨두고 API 서버는 토큰의 변조 여부만 확인하면 된다.
클라이언트는 서버에 다음과 같이 Authorization 헤더를 이용해 JWT를 API 서버로 전달한다.
Authorization: Bearer <token>
API 서버가 클라이언트로부터 JWT를 받게 되면 점(.)을 구분자로 해서 각각 인코드된 헤더, 페이로드, 서명을 얻을 수 있다. 인코드된 헤더와 페이로드를 동일 절차로 해싱하여 해시값을 얻은 다음에 서명과 비교하여 일치하면 변조되지 않은 것이고, 일치하지 않으면 변조된 것이다. 변조되지 않았기 때문에 Auth 서버가 발급한 원본 토큰이라고 믿을 수 있고 API 서버가 더 이상 인증을 신경쓸 필요가 없게 된다.
B64(헤더) . B64(페이로드) . 서명1
JWT 발급 중에 서명을 만든 과정을 동일하게 수행한다. 아까 설명했던 서명 생성 함수를 다시 살펴보자.
HMACSHA256(
B64(헤더) + "." + B64(페이로드),
키)
이 함수를 이용해서 다시 얻은 값을 서명2라고 하자. 받은 JWT 내부의 서명1과 API 서버가 이번에 만들어본 서명2를 비교함으로써 검증 절차가 완료된다.
그런데, HMAC을 사용했는지 RSA를 사용했는지 어떻게 알 수 있을까? 헤더를 base64 디코드해보면 알 수 있다.
Auth 서버와 API 서버의 역할 분리 이외에도, 페이로드에 리소스별 인가(Authorization, 권한 세분화)와 관련된 정보를 담아둘 수 있기 때문에 API 서버가 인증(로그인) 후에 다시 한 번 인가(권한) 절차를 Auth 서버에 의존하지 않아도 된다는 점이 바람직하다.