자바스크립트로 JWT 토큰을 발급하고 검증하기
이번 포스팅에서는 자바스크립트로 어떻게 JWT 토큰을 발급하고 검증하는지에 대해서 알아보겠습니다.
jsonwebtoken 패키지 설치
우선 Node.js의 패키지 매니저인 npm을 이용하여 jsonwebtoken
패키지를 설치하겠습니다.
$ npm i jsonwebtoken
jsonwebtoken
는 JWT 표준 명세서를 자바스크립트 언어로 구현하고 있는 라이브러리입니다.
따라서 JWT 기반으로 사용자 인증이나 인가를 하는 자바스크립트 서버 애플리케이션에서는 직접적으로든 간접적으로든 (passport-jwt
와 같은 프레임워크를 통해서) jsonwebtoken
라이브러리를 사용하게 됩니다.
설치한 jsonwebtoken
패키지는 CommonJS를 모듈 시스템으로 사용하는 자바스크립트 프로젝트에서는 require
키워드로 불러오면 되고요.
const jwt = require("jsonwebtoken");
반면에 ES 모듈 시스템을 사용하는 자바스크립트 프로젝트에서는 import
키워드로 불러올 수 있습니다.
import jwt from "jsonwebtoken";
JWT 토큰
먼저 JWT 토큰을 이용해서 웹에서 서버와 클라이언트가 어떻게 안전하게 사용자 인증/인가 정보를 주고 받는지에 대해서 짚고 넘어가겠습니다.
JWT(JSON Web Token) 토큰은 서버가 로그인을 완료한 클라이언트에게 발급해주는 긴 문자열인데요. 이 문자열에는 사용자의 인증/인가 정보가 담겨있으며 클라이언트는 서버로 요청을 할 때 마다 이 정보를 제공해야 합니다.
서버는 JWT 토큰을 발급할 때 클라이언트에게 보낼 데이터를 반드시 서명(sign)을 하게되어 있는데요. 그래야지 클라이언트가 서버가 JWT 토큰을 보냈을 때 서버에서 토큰을 검증(verify)할 수 있기 때문입니다.
여기서 서명(signing)이라는 작업은 우리가 실생활에서 중요한 계약을 할 때 서명을 한 후에 문서를 주고 받는 것처럼 네트워크 상에서 서버와 클라이언트 간에 데이터를 주고 받을 때 검증 용으로 부수적인 정보를 추가하는 과정을 뜻합니다.
이렇게 검증을 위해 추가된 데이터를 서명(signature)라고 하며 이 서명을 이용하면 서버에서는 송수신 과정에서 데이터의 위변조가 일어나지는 않았는지, 또는 데이터를 돌려주는 주체가 토큰을 받았던 클라이언트가 맞는지 등을 검증할 수 있습니다.
JWT 자체에 대해서는 관련 포스팅에서 자세히 다루고 있으니 참고 바랍니다.
토큰 발급하기
토큰을 발급할 때는 jsonwebtoken
라이브러리에서 제공하는 sign()
함수를 사용하는데요.
첫 번째 인자로 토큰에 담을 JSON 데이터(payload) 두 번째 인자로는 키(key)를 받습니다.
예를 들어서, 이메일 정보를 담고있는 JWT 토큰을 한번 발급해보겠습니다.
const token = jwt.sign({ email: "test@user.com" }, "our_secret");
console.log(token);
그러면 다음과 비슷한 eyJ
로 시작하는 긴 문자열을 얻을 수 있는데요. 이것이 바로 발급된 토큰입니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdXNlci5jb20iLCJpYXQiOjE2Nzg5MjAxMjV9.7agGY4Sx7wWY0vZe25tfsrpIcDUHf5N6XP1W3MfxhWI
여기서 두 번째 인자로 넘기는 키는 나중에 해당 토큰을 검증할 때도 필요합니다. 서명할 때 아무런 설정을 해주지 않으면 HS256이 기본 알고리즘으로 사용되는데 이 대칭키 알고리즘은 암호화와 복호화를 할 때 동일한 키를 사용하기 때문입니다.
만약에 RS256과 같은 비대칭키 알고리즘을 사용하려면 두 번째 인자로 비밀키(private key)를 넘기고, 세 번째 인자를 통해 algorithm
옵션을 명시해주면 됩니다.
const privateKey = fs.readFileSync("private.key");
const token = jwt.sign({ email: "test@user.com" }, privateKey, {
algorithm: "RS256",
});
대신에 이렇게 비대칭키 알고리즘을 사용하면 나중에 토큰을 검증할 때 동일한 키가 아닌 공개키(public key)를 사용해야합니다. (토큰을 발급해주는 서버와 토큰 검증해야하는 서버가 다를 경우 유용하겠죠?)
토큰의 만료 시간을 지정하고 싶다면 세 번째 인자를 통해 expiresIn
옵션을 명시해주면 됩니다.
예를 들어, 1시간 동안 토큰이 유효하길 원하다면 다음과 같이 토큰을 발급합니다.
const token = jwt.sign({ email: "test@user.com" }, "our_secret", {
expiresIn: "1h",
});
토큰 검증하기
JWT 토큰은 jsonwebtoken
라이브러리에서 제공하는 verify()
함수를 사용하여 검증할 수 있는데요.
첫 번째 인자로는 토큰 문자열을 받고, 두 번째 인자로는 sign()
함수와 동일하게 키를 받습니다.
예를 들어서, 토큰을 하나 발급받은 후에 바로 검증한 후 토큰에 저장된 데이터를 출력해보겠습니다.
const token = jwt.sign({ email: "test@user.com" }, "our_secret");
const verified = jwt.verify(token, "our_secret");
console.log(verified);
그러면 sign()
함수에 넘겼던 JSON 데이터 뿐만 아니라 iat
속성이 추가되어 있는 것을 볼 수 있을텐데요.
{ email: 'test@user.com', iat: 1678920125 }
이렇게 JWT 토큰에 부가적으로 저장되는 메타 데이터를 클레임(claim)이라고 합니다.
iat
클레임은 issued at
의 약자로 해당 토큰이 발급된 시각에 대한 유닉스(Unix) 타임스탬프(timestamp)로 담고 있습니다.
만약에 토큰을 발급했을 때와 다른 키를 사용하여 검증을 시도하면 어떻게 될까요?
const token = jwt.sign({ email: "test@user.com" }, "our_secret");
const verified = jwt.verify(token, "your_secret");
console.log(verified);
그러면 서명이 유효하지 않다는 오류가 발생하게 됩니다. 해당 키로 서명을 복호화할 수 없기 때문입니다.
/Users/daleseo/temp/our-jwt/node_modules/jsonwebtoken/verify.js:171
return done(new JsonWebTokenError('invalid signature'));
^
JsonWebTokenError: invalid signature
이번에는 1분 동안 유효한 토큰을 발급한 후에 검증해보겠습니다.
const token = jwt.sign({ email: "test@user.com" }, "our_secret", {
expiresIn: "1m",
});
const verified = jwt.verify(token, "our_secret");
console.log(verified);
그러면 JSON 데이터에 이번에는 iat
클레임과 더불어 exp
클레임이 추가되는 것을 볼 수 있는데요.
exp
클레임은 expiration time
의 약자로 만료 시각을 나타내며 exp
값에서 iat
값을 빼보면 정확히 60초가 나오는 것을 알 수 있습니다.
{ email: 'test@user.com', iat: 1678922236, exp: 1678922296 }
이번에는 만료 시간을 1초로 줄이고 토큰을 발급한 후에 1초 기다렸다가 검증을 해볼까요?
const token = jwt.sign({ email: "test@user.com" }, "our_secret", {
expiresIn: "1s",
});
await new Promise((r) => setTimeout(r, 1000));
const verified = jwt.verify(token, "our_secret");
console.log(verified);
그려면 다음과 같이 토큰이 만료되었다는 오류가 발생할 것입니다.
/Users/daleseo/temp/our-jwt/node_modules/jsonwebtoken/verify.js:190
return done(new TokenExpiredError('jwt expired', new Date(payload.exp * 1000)));
^
TokenExpiredError: jwt expired
토큰 읽기만 하기
토큰을 검증하지 않고 단순히 토큰에 저장된 데이터만 읽고 싶다면 jsonwebtoken
라이브러리에서 제공하는 decode()
함수를 사용할 수 있습니다.
decode()
함수는 검증을 하지 않기 때문에 키를 인자로 받지 않고 그냥 토큰 문자열만 넘기면 됩니다.
예를 들어, 1초 동안만 유효한 토큰을 발급한 다음 1초를 기다린 후 토큰에 저장된 데이터를 읽어서 출력해보겠습니다.
const token = jwt.sign({ email: "test@user.com" }, "our_secret", {
expiresIn: "1s",
});
await new Promise((r) => setTimeout(r, 1000));
const decoded = jwt.decode(token);
console.log(decoded);
그러면 토큰이 만료되었음에도 불구하고 토큰이 담고 있는 JSON 데이터가 출력되는 것을 볼 수 있습니다.
{ email: 'test@user.com', iat: 1678923334, exp: 1678923335 }
페이로드(payload) 뿐만 아니라 헤더(header)와 서명(signature)까지 읽고 싶다면 decode()
함수의 두 번째 인자를 통해서 complete
옵션을 true
로 주면 됩니다.
const token = jwt.sign({ email: "test@user.com" }, "our_secret");
const decoded = jwt.decode(token, { complete: true });
console.log(decoded);
헤더에 담긴 정보를 통해서 토큰 타입이 JWT
이고 토큰이 발급될 때 HS256
알고리즘으로 서명되었다는 것을 알 수 있습니다.
{
header: { alg: 'HS256', typ: 'JWT' },
payload: { email: 'test@user.com', iat: 1678923622 },
signature: 'Ppj2VqDi5XTY3pE3zUzbHa2DgQBRAVsQ14kwMlpBOXE'
}
마치면서
지금까지 jsonwebtoken
라이브러리를 사용해서 JWT 토큰을 발급하고 검증, 그리고 단순히 토큰에 저장된 데이터를 읽는 방법에 대해서 살펴보았습니다.
참 이게 알고보면 간단한데, 보통 다른 프레임워크를 통해서 간접적으로 사용하는 경우가 많다보니 의외로 어렵게 느껴질 수 있는 것 같아요.
본 포스팅이 JWT의 본질과 좀 더 가까워지는데 도움이 되었으면 좋겠습니다.
JWT에 연관된 포스팅은 JWT 태그를 통해서 쉽게 만나보세요!