JWT는 큰 그림에서 생각하면 서버가 유저의 인증-인가 과정에서 사용 가능한 매개 수단이다.
인증과 인가는 단어도 비슷하지만 분명히 다른 신원 확인 및 접근 통제 방법이다.
- 인증: 유저가 유저가 주장하는 자기 자신이 맞는가를 검증하는 과정
- 인가: 유저에게 리소스에 접근 가능한 권한을 부여하는 것
편의점에 맥주를 사러 가서 신분증을 확인하는 상황을 비유해보자면, 편의점 알바생은 다음과 같은 인증-인가 과정을 거친다고 볼 수 있다.
우리가 어떤 서비스나 앱을 설치해서 처음 하는 일이 아이디와 패스워드를 발급하는 것을 보면 알 수 있듯이, 인증 과정은 모든 보안 프로세스에서 가장 첫 부분을 차지한다. 먼저 누가 누구인지 알고 등록해야 다음 과정이 가능하다.
인증을 가능하게 하는 수단은 여러가지가 있지만 보통 다음과 같은 것이 사용된다.
인증 수단마다 각자 장단점이 있지만 이들을 조합해서 사용하는 멀티팩터인증(Multi-factor Authentication)을 사용하면 단일 수단보다 훨씬 증가한 보안을 제공할 수 있다.
인가는 반면 유저가 어떤 서비스의 특정 자원이나 함수를 사용할 권한이 있는가를 결정해준다.
내 집에 손님을 들어오는 손님을 내가 인증해주고 들여보냈다고 해서 손님이 해당 집의 모든 자원에 대한 권한을 갖는게 아니듯, 유저마다 권한이 다르고 이를 부여하는 과정을 인가라고 한다.
권한을 줄 때 누구에게 줄 것인지 정하지 않으면 인가 자체가 의미를 가질 수 없기에, 인가는 항상 인증 뒤에 따라올 수 있다.
JSON Web Token(JWT)는 JSON 포맷을 이용한 두 객체간의 (예를들어, 서버와 클라이언트) 간결하고 완전한(self-contained) 정보 교환 방식으로, RFC 7519 표준으로 정의된다. (LINK)
우리는 JWT를 위에서 말한 인증 또는 인가, 또는 안전한 정보 교환 방식으로 사용할 수 있다.
유저가 인증을 받고 로그인에 성공하면 서버는 유저에게 JWT를 전송하며 해당 유저가 서비스에서 갖는 권한들을 부여 할 수 있다.
또, JWT는 퍼블릭/프라이빗 키 쌍으로도 서명될 수 있기에 JWT의 수신자는 송신자의 아이덴티티를 확인할 수 있고, 해당 서명과 메세지 내용간 해시 함수의 계산을 통해 메세지의 무결성 또한 확인할 수 있다.
위의 어려운 내용만 생각해보면 JWT는 정말 복잡할 것 같지만, 사실 구조 자체는 '.'
으로 나뉜 세 부분의 문자열로 매우 단순하다.
<헤더>.<페이로드>.<서명>
JWT의 헤더는 다음과 같은 정보를 포함하는 Base64URL
인코딩이다.
JWT
이다. 예)
{
"alg": "HS256",
"typ": "JWT"
}
페이로드는 클레임
들로 구성된 또 다른 json의 Base64URL
인코딩으로, 각 클레임은 종류에 따라 registered
, public
, private
의 세 가지 타입이 있다. (반드시 한 페이로드에 세 종류가 다 존재해야 하는 것은 아니다.)
페이로드 자체의 정보는 뒤에 붙는 서명 파트 덕에 내용이 변조될 시 발급자가 알아챌 수 있지만, 페이로드의 내용 자체는 토큰을 가진 누구나 열어볼 수 있다. (암호화 되지 않는다.)
그러므로 JWT 자체를 암호화하지 않는한, 페이로드에 절대로 비밀 정보를 포함시켜서는 안된다.
예)
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
앞선 헤더와 페이로드 부분의 변조를 알아챌 수 있도록 JWT의 마지막 부분에는 헤더와 페이로드의 내용을 가지고 송신자가 서명을 만들어서 붙인다.
signature = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
만약 토큰이 프라이빗-퍼블릭 키 페어로 서명된다면 토큰의 변조 여부 뿐만 아니라 송신자 또한 검증할 수 있다.
앞서 만든 헤더, 페이로드, 서명 부분을 모두 Base64URL 형식으로 인코딩 후 '.'
으로 구분해서 붙여주면 JWT가 완성되며, 이는 손쉽게 HTML 또는 HTTP 환경을 통해 전달될 수 있다.
이곳에서 JWT를 직접 인코딩 해 볼 수 있다.
성공적으로 인증을 통과한 유저에게 서버는 JWT를 생성해서 전달할 수 있다. 나중에 유저가 서비스의 인가가 필요한 영역에 접속할 때 유저는 HTTP 요청의 Authorization
헤더에 가지고 있는 토큰을 포함시켜 인가를 요청할 수 있다.
이 경우 서버는 토큰을 받아서 서버가 가진 시크릿키와 헤더 + 페이로드 부분의 해시 값이 서명과 동일한지 여부를 따져서 토큰이 서버에서 발행되었는지, 또 유효한 토큰인지 여부를 확인할 수 있다.
페이로드에 유저의 아이디에 대한 정보가 담겨있다면 따로 DB를 조회할 필요 없이 바로 사용자를 확인할 수도 있다.
이 경우에 JWT 자체가 유저의 인증서를 의미하기 때문에 토큰은 발급 후 한시적인 시간 동안만 유효하게 쓰여야 한다.
JWT는 서버를 Stateless한 상태로 유지할 수 있다. 즉 서버가 누가 로그인해있고 누구에게 어떤 권한을 주었는지 모두 저장하지 않아도 된다.
또한 따로 인증 서버나 데이터베이스를 두지 않고도 쉽게 애플리케이션 서버를 확장할 수도 있다.
다만, 앞서도 말했듯 페이로드 자체는 base64로 인코딩 될 뿐 암호화는 되지 않기에 보안상 중요한 정보를 포함할 수 없다.
또한, 일단 발급된 토큰은 유효기간이 만료되기 전까지는 탈취되어도 무방비로 사용될 수 있다는 단점이 있다.
이런 단점을 보완하기 위해 토큰을 엑세스 토큰과 리프레시 토큰으로 나누고 엑세스 토큰에는 리프레시 토큰보다 짧은 유효기간을 주어 엑세스 토큰이 만료되었을 시 리프레시 토큰으로만 엑세스 토큰을 재발급 받는 방법을 쓰기도 한다.
이 방법으로 리프레시 토큰은 엑세스 토큰의 재발급 시에만 쓰이므로 탈취 위험을 낮출 수 있고 엑세스 토큰이 탈취당하더라도 짧은 유효기간에서밖에 사용할 수 없고 어차피 리프레시 토큰이 없으면 엑세스 토큰을 재발급 받을 수 없으니 보안 위협을 낮출 수 있다.
하지만 위 경우도 리프레시 토큰이 탈취되는 경우는 보안 상 문제가 생기는 것은 어쩔 수 없다. (사실 ID/Password가 탈취된 경우를 상정해서 보안하기 힘든 것과 같은 문제다.)