치춘짱베리굿나이스

jsonwebtoken 본문

ServerSide/사용해본 라이브러리

jsonwebtoken

치춘 2022. 10. 11. 01:04

jsonwebtoken

설치

$> npm i jsonwebtoken
$> npm i -D @types/jsonwebtoken // 당신이 타입스크립트 사용자라면

$> yarn add jsonwebtoken
$> yarn add -D @types/jsonwebtoken // 당신이 타입스크립트 사용자라면

npm 링크

https://www.npmjs.com/package/jsonwebtoken

 

jsonwebtoken

JSON Web Token implementation (symmetric and asymmetric). Latest version: 8.5.1, last published: 4 years ago. Start using jsonwebtoken in your project by running `npm i jsonwebtoken`. There are 21099 other projects in the npm registry using jsonwebtoken.

www.npmjs.com

yarn 링크

https://yarnpkg.com/package/jsonwebtoken

 

https://yarnpkg.com/package/jsonwebtoken

Fast, reliable, and secure dependency management.

yarnpkg.com

용례

JWT를 손쉽게 제작 및 검증할 수 있는 라이브러리이다

https://chichoon.tistory.com/761 JWT에 관한 이론 글을 적었었는데, 이번에는 실제로 JWT 토큰을 만들어 본다

jwt 모듈 불러오기

import jwt from 'jsonwebtoken';

jwt 모듈을 불러와 메서드를 사용하기 위해 점을 찍어보면 decode, sign, verify 그리고 에러 선언 3개 총 6개의 메서드와 프로퍼티가 정의되어 있는 것을 볼 수 있다

메서드 이름만 봐도 너무 직관적이다! 정말 사용하기 쉽고 간단한 라이브러리다

처음에는 jwt 모듈을 사용해서 토큰을 만들고 검증까지 해 보고, 뒤에는 jwt 모듈을 직접 만들어보는 것도 좋을 듯하다

jwt.sign

const token = jwt.sign(payload, secretKey);

페이로드 객체와 비밀 키 (검증할 때 사용할 secret 키) 를 인자로 넣어주면 토큰을 반환한다

헤더를 따로 넣어주지 않는 이유는, 헤더에 들어가는 정보는 토큰의 타입 (JWT로 고정) 과 서명을 암호화할 때 사용하는 알고리즘 (HS256 등) 밖에 없으므로 모듈이 알아서 다 넣어주기 떄문이다

비밀 키는 검증 시에 이 서버에서 만든 토큰이 맞는지? (조작된 토큰이 아닌지) 체크하는 용도로 으로, 보안을 위해서라면 파일을 읽어오거나 환경변수로 복잡한 문자열을 지정하여 사용하는 것이 좋다

jwt.sign (with options)

const token = jwt.sign(payload, secretKey, options);

세 번째 인자로 옵션 객체를 넣을 수 있다

옵션 객체는 아래의 key-value 쌍을 가질 수 있다

  • algorithm : 서명을 암호화할 알고리즘 (비워두면, 기본값으로 HS256이 적용된다)
  • expiresIn : 이 토큰이 만료되는 시점
    • 토큰 발행 시점부터 얼마만큼의 시간 동안 토큰이 유효한지 (얼마만큼의 시간이 흘러야 만료되는지) 지정하는 값이다
    • 시간 단위를 함께 사용할 수도 있고, 단위를 붙이지 않는다면 기본값으로 ms 단위로 설정된다
    • 2h, 60 (60ms), 10d
    • 등록된 클레임 (reserved claim) 중 exp와 같은 값으로, 해당 옵션 설정과 페이로드의 exp 설정을 동시에 할 수 없다
  • notBefore : 이 토큰이 활성화되는 시점
    • 토큰 발행 시점부터 얼마만큼의 시간이 흘러야 토큰이 활성화되는지 지정하는 값이다
    • expiresIn과 마찬가지로 시간 단위를 함께 붙여 사용한다
    • 등록된 클레임 중 nbf와 같은 값으로, 해당 옵션 설정과 페이로드의 nbf 설정을 동시에 할 수 없다
  • audience : JWT가 의도하는 수신자
    • 등록된 클레임 중 aud와 같은 값으로, 해당 옵션 설정과 페이로드의 aud 설정을 동시에 할 수 없다
  • issuer : JWT를 발급한 주체 (발신자)
    • 등록된 클레임 중 iss와 같은 값으로, 해당 옵션 설정과 페이로드의 iss 설정을 동시에 할 수 없다
  • subject : 이 토큰의 주제
    • 등록된 클레임 중 sub와 같은 값으로, 해당 옵션 설정과 페이로드의 sub 설정을 동시에 할 수 없다
  • jwtid : 이 토큰의 식별자
    • 등록된 클레임 중 jti와 같은 값으로, 해당 옵션 설정과 페이로드의 jti 설정을 동시에 할 수 없다
  • subject : 이 토큰의 주제
    • 등록된 클레임 중 sub와 같은 값으로, 해당 옵션 설정과 페이로드의 sub 설정을 동시에 할 수 없다
  • noTimestamp : issueAt (iat) 자동 설정 여부
    • 기본값은 false (issueAt 자동 설정) 으로, true일 경우 iat 값이 설정되지 않는다
    • 이 값을 true로 설정하면 페이로드에 iat을 직접 넣을 수 있다
    • 실제 토큰 발행 시점이 아니라 지정한 시각을 발행 시점으로 사용할 수 있는 것
  • header : 헤더 커스터마이징
    • 기본 헤더 설정 대신 원하는 대로 헤더에 값을 수정 및 추가할 수 있다
  • keyid
  • mutatePayload: true로 설정될 경우, sign 메서드가 페이로드 객체를 직접 수정할 것이다
    • 등록된 클레임 중 iat은 옵션 설정 없이도 자동으로 페이로드에 포함되므로, mutatedPayload가 참일 경우 sign 메서드의 첫 번째 인자로 받은 객체에 iat 프로퍼티가 자동으로 추가된다
    • 그 외에도, 옵션을 통해 추가한 클레임들이 자동으로 객체에 추가된다
    • 토큰에 인코딩되기 직전 페이로드 값을 알고 싶다면 true로 설정하라고 공식 문서에 적혀 있다

 

import jwt from 'jsonwebtoken';

const secretKey = '시크릿키'; // 가급적 환경변수 또는 별도 파일에 분리 추천

const obj = { id: 'chichoon', nickname: '치춘' };
const token = jwt.sign(obj, secretKey, {
    issuer: 'chichoon',
    jwtid: 'asd',
    subject: 'asd'
});
console.log(token);

토큰 발급이 완료되었다

정말 사용하기 쉬운 라이브러리다… 직관성 100점

이번에는 사용법을 정리하기 위해 시크릿 키를 대충 파일 상단에 선언만 했지만, 말 그대로 ‘시크릿 키' 인 만큼 보안을 위해 파일이나 환경변수에 분리하는 것이 좋겠다 (깃허브에 저 값이 그대로 올라가면….)

 

필자는? 보통 dotenv 모듈을 이용하여 .env 파일에 분리하는 방식을 택하곤 한다

굳이 파일 열어서 내부 문자열 읽어들이고… 하는 복잡한 과정을 구현할 필요 없이 알아서 환경변수 (process.env) 에 등록해 주어서 편하다

 

https://jwt.io/ 의 디버거를 이용하면 base64를 다시 복호화해서 내용물을 열어볼 수 있다

위에 내가 설정한 값 (id, nickname, iss, sub, jti) 과 동일하게 잘 들어간 것을 확인할 수 있다

. 단위로 헤더 - 페이로드 - 서명 연결까지 해주니 이렇게 편할 수가 없다

jwt.verify

const decoded = jwt.verify(jwtToken, secretKey);

토큰을 만들었으니, 이 토큰이 진실된 (?) 토큰인지 알아볼 차례이다

jwtToken에는 토큰을 넣고, secretKey에는 토큰 생성 시에 사용했던 시크릿 키를 똑같이 사용하면 된다

같은 secretKey를 넣어줌으로써 이 토큰의 서명 부분이 해당 시크릿 키를 이용하여 인코딩되었는지 = 우리가 작성한 토큰이 맞는지, 조작되진 않았는지 알 수 있다

검증에 성공했을 경우 (값이 조작되지 않았고, 우리가 생성한 토큰이 맞을 경우) 페이로드 부분을 복호화하여 원본 객체로 반환해 주고, 실패했을 경우 예외를 throw 한다

 

import jwt from 'jsonwebtoken';

const secretKey = '시크릿키'; // 가급적 환경변수 또는 별도 파일에 분리 추천

...
try {
    const decoded = jwt.verify(jwtToken, secretKey);
    console.log(decoded);
} catch (e) {
    console.log(e);
}

예외 때문에 try-catch 문을 통해 예외를 잡아줘야 한다

아까 맨 처음에 보았듯 검증 실패에 대한 각각의 예외가 별도로 지정되어 있으므로, 그에 맞는 에러 문구를 받아볼 것이다

 

  1. 아직 JWT가 활성화되지 않았을 경우 (NotBeforeError)

현재 시각이 JWT가 활성화되기로 한 시각 (nbf) 이전일 때 발생하는 예외이다

토큰 자체가 활성화되어 있지 않으므로 검증에 실패한 것이다

 

  1. JWT 토큰이 만료되었을 경우 (TokenExpiredError)

위의 경우와 반대로, 현재 시각이 JWT가 만료되는 시각 (exp) 이후일 때 발생하는 예외이다

토큰이 이미 만료되었으므로 검증에 실패하였다

 

  1. 어딘가 나사가 빠진 토큰 (JsonWebTokenError: invalid token)

토큰의 형식 (헤더.페이로드.서명) 은 지키되, 토큰에 문자를 추가하거나 중간의 글자를 제거하는 식으로 재현해볼 수 있다

 

  1. 유효하지 않은 서명 (JsonWebTokenError: invalid signature)

시크릿 키가 다르거나, 페이로드가 조작되는 등의 이슈로 토큰의 서명 부분이 jwt.verify에서 헤더와 페이로드를 가지고 직접 암호화한 서명 부분과 일치하지 않을 때 발생하는 예외이다

토큰이 조작되었다던가, 우리가 보낸 토큰이 아닐 때 등 보안 이슈로 걸러지는 경우이다

 

  1. 이건 토큰도 아닌데 (JsonWebTokenError: jwt malformed)

JWT 토큰 형식이 아닌 문자열이 들어왔을 경우 발생하는 예외이다

대표적으로 헤더와 페이로드, 서명이 .을 기준으로 이어져 있지 않을 경우 (. 개수가 부족한 경우) 또는 헤더와 페이로드, 서명 중 하나라도 누락되었을 경우 등이 있다

 

  1. 허용되지 않은 알고리즘으로 암호화된 서명 (JsonWebTokenError: invalid algorithm)

verify 메서드의 세 번째 인자로 들어가는 옵션 객체의 algorithms 속성과 관련이 있다

서명 부분을 암호화하는 데 사용된 알고리즘이 algorithms 배열 내에 없는 다른 알고리즘으로 서명 부분을 암호화했을 경우 발생하는 예외이다

 

  1. 허용되지 않은 수신자 (JsonWebTokenError: jwt audience invalid. expected: <예상한 수신자>)

verify 메서드의 세 번째 인자로 들어가는 옵션 객체의 audience 속성과 관련이 있다

토큰의 수신자 (aud) 가 audience 배열 내에 없을 경우, 또는 정규식과 일치하지 않을 경우 발생하는 예외이다

 

  1. 허용되지 않은 발신자 (JsonWebTokenError: jwt issuer invalid. expected: <예상한 발신자>)

verify 메서드의 세 번째 인자로 들어가는 옵션 객체의 issuer 속성과 관련이 있다

토큰의 발신자 (iss) 가 issuer와 다르거나 배열 내에 없을 경우 발생하는 예외이다

 

  1. 허용되지 않은 식별자 (JsonWebTokenError: jwt jwtid invalid. expected: <예상한 식별자>)

verify 메서드의 세 번째 인자로 들어가는 옵션 객체의 jwtid 속성과 관련이 있다

토큰의 식별자 (jti) 가 jwtid와 다를 경우 발생하는 예외이다

 

  1. 허용되지 않은 주제 (JsonWebTokenError: jwt subject invalid. expected: <예상한 주제>)

verify 메서드의 세 번째 인자로 들어가는 옵션 객체의 subject 속성과 관련이 있다

토큰의 주제 (sub) 가 subject와 다를 경우 발생하는 예외이다

 

그 외의 경우, 정상적으로 복호화한 페이로드 부분을 반환한다

페이로드 외의 부분 (헤더, 서명) 도 보고 싶다면 하단의 옵션을 지정해줘야 한다

jwt.verify (with options)

const decoded = jwt.verify(jwtToken, secretKey, options);

마찬가지로 세 번째 인자로 옵션 객체를 넣을 수 있다

아래와 같은 key-value 쌍을 가질 수 있다

  • algorithms : 배열로 허용 알고리즘 종류를 지정할 수 있다
    • algorithms: [”HS256”, “HS384”] 이런 식
    • 만약 토큰의 서명 부분이 지정한 알고리즘 종류와는 다른 알고리즘으로 암호화되었다면, JsonWebTokenError: invalid algorithm 예외가 발생한다
    • 지원하는 알고리즘 종류는 공식 문서에 표로 나와 있다
  • audience : 문자열, 정규식 또는 문자열 배열, 정규식 배열로 토큰 수신 대상자 (aud) 를 지정할 수 있다
    • audience: [”chichoon”, “jiychoi”, /jiy.*/] 이런 식
    • 만약 토큰의 aud 가 배열 내에 없을 경우 (또는 정규식과 일치하지 않을 경우) JsonWebTokenError: jwt audience invalid. expected: <예상한 수신자> 예외가 발생한다
  • issuer : 문자열 또는 문자열 배열로, 토큰을 생성한 사람 (iss) 을 지정할 수 있다
    • issuer: [”chichoon”, “jiychoi”] 이런 식
    • 만약 토큰의 iss가 배열 내에 없을 경우 JsonWebTokenError: jwt issuer invalid. expected: <예상한 발신자> 예외가 발생한다
  • jwtid : 문자열로, 토큰의 식별자 (jti) 를 지정할 수 있다
    • jwtid: ‘hello’ 이런 식
    • 만약 토큰의 jtijwtid 값이 일치하지 않을 경우 JsonWebTokenError: jwt jwtid invalid. expected: <예상한 식별자> 예외가 발생한다
  • subject : 문자열로, 토큰의 주제 (sub) 를 지정할 수 있다
    • subject: ‘token’ 이런 식
    • 만약 토큰의 subsubject 값이 일치하지 않을 경우 JsonWebTokenError: jwt subject invalid. expected: <예상한 주제> 예외가 발생한다
  • ignoreExpiration: true일 경우, 토큰의 만료 여부 (exp) 는 검증하지 않는다
  • ignoreNotBefore: true일 경우, 토큰의 비활성 여부 (nbf) 는 검증하지 않는다
  • clockTolerance: 만료 여부 (exp) 와 비활성 여부 (nbf) 를 검증할 때, 서버간 시간차를 보정해주기 위한 옵션이다
  • maxAge: 토큰이 유효하다고 판단하기 위한 추가 시간을 지정할 수 있다
  • clockTimestamp: 현재 시간 대신 검증에 사용할 시간을 따로 지정할 수 있다
  • nonce: nonce 클레임을 검증할 수 있는 속성이라고 한다

 

  • complete : 기존처럼 페이로드만 반환하는 대신, { header, payload, signature } 형식의 객체를 반환한다
    • 위의 예시처럼 헤더와 페이로드는 복호화한 객체로 반환한다
    • 서명 부분은 복호화할 수 없으므로 (단방향 암호화 하였으므로) 문자열 그대로 보여준다

jwt.verify (with callback)

const decoded = jwt.verify(jwtToken, secretKey, options, callback);

네 번째 인자로 콜백 함수를 지정할 수 있다

이 콜백 함수는 검증에 성공했을 때 (복호화된 값) 와 검증에 실패했을 때 (발생한 예외) 를 인자로 구분하여 서로 다른 행동을 취할 수 있도록 설정할 수 있다

 

function verifyCallback(error, decoded) {
    if (error) {
        // 예외 발생 (검증 실패)
    }
    if (decoded) {
        // 검증 성공
    }
}

콜백 함수는 위와 같은 형태로 인자 두 개를 받는다

따라서 내부에서 if문을 통해 검증 성공 여부를 분기로 나눠 처리할 수 있는 것이다

만약 4번째 인자로 콜백 함수가 지정된다면, verify 메서드는 콜백 함수의 반환값을 반환한다 (반대로 말하면, 콜백 함수의 반환값이 없을 경우 verify 메서드는 undefined를 반환한다)

 

또한 try-catch 대신 콜백 함수 내에서 예외를 처리하므로, 콜백 함수 내에서 throw를 하지 않는 이상 try-catch 문이 필요가 없어진다

로직을 다른 모듈로 분리하고 싶다면 콜백 함수를 분리해서 인자로 넣어주고, 간단하게 예외처리만 하고 싶다면 try-catch를 사용하면 되겠다.. 취향껏

jwt.decode

const decoded = jwt.decode(token);

검증 과정 일절 없이 복호화만 해 주는 함수이다

만약 유효하지 않은 토큰일 경우 (헤더 - 페이로드 - 서명 구조가 아니거나, 잘못된 base64 문자열이라 복호화가 불가능한 등) 예외 발생 없이 null을 반환한다

검증을 하지 않으므로 서명이 깨져도 신경쓰지 않는다

두 번째 인자로 옵션을 지정해 줄 수도 있는데,

  • complete: verify 메서드의 complete 옵션과 같다
    • 페이로드 뿐만 아닌 헤더와 서명도 같이 반환한다
  • json: boolean 값을 받으며, 시험해 봤는데 별 차이가 없는 듯 하다… json 형식으로 반환할지 여부를 묻는건지…?

공식 문서에는 decode 메서드 관련 내용이 없는데, verify 메서드 내에서 내부적으로 사용되는 메서드가 아닐지… 생각이 들기도

여담

구성이 매우 간단해서 직접 구현해 보면서 JWT 형식을 이해하기에도 좋은 라이브러리인 것 같다 (시간이 된다면…)

깃허브 스타 15800개 가량으로 엄청난 인기를 구가하고 있는데 그 이유를 알 것 같다 (심플 이즈 베스트)


참고자료

https://github.com/auth0/node-jsonwebtoken

Comments