[JWT] JWT를 파헤쳐보자

Hocaron·2022년 2월 2일
0

JWT

내가 아는 jwt는 아래가 전부였다.🙄

  • header
{
  "alg": "HS256",
  "typ": "JWT"
}
  • payload
{
  "sub": 1,
  "iat": 1641550513,
  "exp": 4233550513
}
  • verify signature

옵션이 굉장히 다양하다고?

jsonwebtoken를 보면 이런 코드가 나온다.

  • header
export interface JwtHeader {
    alg: string | Algorithm;
    typ?: string | undefined;
    cty?: string | undefined;
    crit?: Array<string | Exclude<keyof JwtHeader, 'crit'>> | undefined;
    kid?: string | undefined;
    jku?: string | undefined;
    x5u?: string | string[] | undefined;
    'x5t#S256'?: string | undefined;
    x5t?: string | undefined;
    x5c?: string | string[] | undefined;
}
  • payload
export interface JwtPayload {
    [key: string]: any;
    iss?: string | undefined;
    sub?: string | undefined;
    aud?: string | string[] | undefined;
    exp?: number | undefined;
    nbf?: number | undefined;
    iat?: number | undefined;
    jti?: string | undefined;
}

payload에도 종류가 있다.

  • 등록된 (registered) 클레임
    • iss: 토큰 발급자 (issuer)
    • sub: 토큰 제목 (subject)
    • aud: 토큰 대상자 (audience)
    • exp: 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1480849147370) 당연하지만 현재 시간보다 이후로 설정되어있어야한다.
    • nbf: Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념이다.
    • iat: 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있다.
    • jti: JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용됩니다. 일회용 토큰에 사용하면 유용하다.
  • 공개 (public) 클레임
    • 공개 클레임들은 충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 한다. 충돌을 방지하기 위해서는, 클레임 이름을 URI 형식으로 짓는다.

      사실 단순히 서버와 클라이언트 사이에서 사용자를 인증하는 용도로 사용한다면 크게 신경쓰지 않아도 좋다. 서버-클라이언트 사이의 단순 통신을 넘어 제 3자도 JWT 토큰을 사용할 때 충돌이 일어나지 않도록 합의된 클레임이라고 생각하면 된다.

  • 비공개 (private) 클레임
    • 등록된 클레임도아니고, 공개된 클레임들도 아니다. 양 측간에 (보통 클라이언트 <->서버) 협의하에 사용되는 클레임 이름이다.

JWT를 만들어보자.

  • json 형태의 객체를 Base64 로 인코딩 해주는 함수
function base64(json) {
  // JSON을 문자열화
  const stringified = JSON.stringify(json);
  // 문자열화 된 JSON 을 Base64 로 인코딩
  const base64Encoded = Buffer.from(stringified).toString("base64");
  // Base 64 의 Padding(= or ==) 을 제거
  const paddingRemoved = base64Encoded.replaceAll("=", "");
  return paddingRemoved;
}

Base64 로 문자열을 인코딩 하면, 결과물 마지막에 = 혹은 == 가 가끔 같이 나오는 경우가 존재한다. 이를 Padding 이라고 하는데, 이를 제거하지 않으면 URL Safe 하지 않게 되므로 반드시 제거하자. 제거해도 Decode 를 정상적으로 할 수 있다.

  • header를 만들어보자.
const header = {
  alg: "HS256",
  typ: "JWT",
};

const encodedHeader = base64(header);
  • payload를 만들어보자.
const payload = {
  email: "hocaron@hocaron.com",
  name: "hocaron",
};
const encodedPayload = base64(payload);
// eyJlbWFpbCI6ImRldmh1ZGlAZ21haWwuY29tIiwibmFtZSI6Ikh1ZGkiLCJpc0FkbWluIjp0cnVlfQ
  • signature를 만들어보자.
const signature = crypto
  .createHmac("sha256", "secret_key")
  .update(`${encodedHeader}.${encodedPayload}`)
  .digest("base64")
  .replaceAll("=", "");

HMAC (Keyed-hash Message Authentication Code) 이란, 메시지 인증 코드 (MAC) 의 한 유형으로서 특정 Key 와 함께 특정 Message 를 Hash 값으로 만드는 암호화 방식이다. 공격자로 하여금 레인보우 테이블 기법의 해킹을 어렵게 하기 위해 원문과 함께 비밀키를 더하여 해싱하는 것 이다.

  • 조합하면 jwt가 만들어진다.
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhvY2Fyb25AaG9jYXJvbi5jb20iLCJuYW1lIjoiaG9jYXJvbiIsImlhdCI6MTUxNjIzOTAyMn0.Zt8PAM872GjhXezWR1booXAF487wf5SOXDtFPCgXKHE

검증은 여기서

https://jwt.io/

JWT를 버전별로 관리할 수 있다고?

원래는 jwt payload에 userId만 있었는데, email로 변경해서 인증해야하는 api가 있다고 하자. 여기에 대응하기 위해, jwt header에 version을 기입해서 관리한다고 한다.

References

profile
기록을 통한 성장을

0개의 댓글