Logo

자바스크립트로 JWT 토큰을 발급하고 검증하기

이번 포스팅에서는 자바스크립트로 어떻게 JWT 토큰을 발급하고 검증하는지에 대해서 알아보겠습니다.

jsonwebtoken 패키지 설치

우선 Node.js의 패키지 매니저인 npm을 이용하여 jsonwebtoken 패키지를 설치하겠습니다.

$ npm i jsonwebtoken

jsonwebtokenJWT 표준 명세서를 자바스크립트 언어로 구현하고 있는 라이브러리입니다. 따라서 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 태그를 통해서 쉽게 만나보세요!