express로 라이브러리 안쓰고 JWT 구현하기

Song-Minhyung·2023년 9월 13일

Javascript

목록 보기
8/8

JWT란?

Json Web Token의 약자입니다.
json 포맷을 사용해 사용자의 속성을 저장하는 web token 입니다.
그리고 jwt는 주로 회원 인증을 위해 사용됩니다.

구조는 아래처럼 header, payload, signature로 이루어졌있고,
각 부분은 base64로 인코딩되어 있으며, .을 사용해 구분을 해줍니다.

구조

헤더는 algtyp으로 이루어져있는데 typ은 토큰의 타입을 의미하며 alg는 signature를 해싱할 때 사용되는 알고리즘을 적어둡니다.

payload

토큰에서 사용할 조각(claim) 들이 들어가는 곳 입니다.
등록된 클레임과 공개 클레임 비공개 클레임으로 나누어집니다.
등록된 클레임은 아래와 같습니다.

iss: issuer - 토큰 발급자
sub: subject - 토큰 제목
aud: audience - 토큰 대상자
exp: expiration - 토큰 만료일
nbf: not before - 토큰 활성 날자
iat: issued at - 토큰 발급 시간
jti: JWT ID - 토큰 식별자

그리고 공개 클레임은 uri 포맷을 이용하며
비공개 클레임은 서버 <-> 클라이언트 통신을 위해 사용자가 임의로 지정한 클레임입니다.

signature

토큰을 검증할 때 사용하는 고유한 암호화 코드입니다.
이를 사용해서 유효한 토큰인지 검증을 하게됩니다.

토큰생성

본격적으로 토큰을 생성해주기 전에 base64로 인코딩 해주는 함수를 먼저 만들어줍니다.
여기서 = 을 모두 제거해주는 이유는 혹시라도 발생할 충돌을 방지해주기 위해서 입니다.
만약 url에 = 이 포함된 토큰을 넣을경우 문제가 발생할 수 있습니다.

function toBase64(obj) {
  const stringifiedObj = JSON.stringify(obj);

  const base64Encoded = Buffer.from(stringifiedObj).toString('base64');

  const paddingRemoved = base64Encoded.replaceAll('=', '');

  return paddingRemoved;
}

그리고 드디어 토큰 발급 생성입니다.
우선 헤더를 만들어줍니다.
간단합니다. alg와 typ을 넣고 base64로 인코딩하면 됩니다.

function sign(payloads, expire) {
  ...
  const encodedHeader = toBase64({
    alg: algorithm,
    typ: 'JWT',
  });
}

그 후 payload를 만들어줍니다.
원하는 값들을 넣고 이역시 base64로 인코딩 해줍니다.

function sign(payloads, expire) {
  ...
  const nowTime = new Date().getTime();
  const encodedPayload = toBase64({
    ...payloads,
    iat: nowTime,
    exp: nowTime + expire,
    iss: issuer,
  });
}

다음으로 제일 중요한 signature를 생성해줄 차례입니다.
nodejs의 crypto를 사용해 sha256으로 해싱해줍니다.
여기서 secret은 중요합니다. 지금은 그냥 넣어줬지만
검증할 때 사용되므로 안전한곳에 보관 해 두어야 합니다.

import { createHmac } from 'crypto';
function sign(payloads, expire) {
  ...
  const encodedSignature = createHmac('sha256', 'secret')
    .update(`${encodedHeader}.${encodedPayload}`)
    .digest('base64')
    .replaceAll('=', '');
}

마지막으로 이렇게 만든 header, payload, signature를 합치면 끝입니다.

function sign(payloads, expire) {
  ...
  const jwt = `${encodedHeader}.${encodedPayload}.${encodedSignature}`;

  return  jwt;
}

아래는 전체 코드입니다.

function sign(payloads, expire) {
  const encodedHeader = toBase64({
    alg: algorithm,
    typ: 'JWT',
  });

  const nowTime = new Date().getTime();
  const payload = {
    ...payloads,
    iat: nowTime,
    exp: nowTime + expire,
    iss: issuer,
  };
  const encodedPayload = toBase64(payload);

  const encodedSignature = createHmac('sha256', secret)
    .update(`${encodedHeader}.${encodedPayload}`)
    .digest('base64')
    .replaceAll('=', '');

  const jwt = `${encodedHeader}.${encodedPayload}.${encodedSignature}`;

  return jwt;
}

토큰 검증

검증은 생성 과정을 반대로 하면 됩니다.

function verify(token) {
  const [encodedHeader, encodedPayload, encodedSignature] = token.split('.');

  const { alg, typ } = toUTF8(encodedHeader);
  const payload = toUTF8(encodedPayload);
  const { iat, exp, iss, email, type } = payload;

  const newSignature = createHmac('sha256', secret)
    .update(`${encodedHeader}.${encodedPayload}`)
    .digest('base64')
    .replaceAll('=', '');

  if (!this.isValidToken(typ, iss, encodedSignature, newSignature, alg)) {
    throw new Error('INVALID TOKEN');
  }
  if (this.isExpiredToken(exp, iat)) {
    throw new Error('EXPIRED TOKEN');
  }

  return payload;
}

function isValidToken(typ, iss, encodedSignature, newSignature, alg) {
  return (
    typ === 'JWT' && iss === issuer && encodedSignature === newSignature && alg === algorithm
  );
}
function isExpiredToken(exp, iat) {
  return exp - iat <= 0;
}

한가지 눈여겨 봐야할 부분은
유효한 토큰인지 확인하는 isValidToken
기간이 만료되었는지 확인하는 isExpiredToken 함수입니다.

만약 유효하고 기간이 만료되지 않았다면 utf8로 디코딩된 payload를 리턴해줍니다.

이렇게 완성된 토큰은 https://jwt.io/ 에서 이게 제대로된 토큰인지 확인할 수 가 있습니다.

profile
기록하는 블로그

0개의 댓글