본문 바로가기
JS

Client에서 JWT 생성기 (JSON Web Token, Frontend)

by 사과넹 2024. 4. 15.
반응형

JWT 정의

  • 늘려서 JSON Web Token
  • JSON 객체를 활용하여 안전하게 정보를 전송할 수 있는 간결하고 독립적인 개방형 표준이다.

아래는 jwt.io에서 발췌한 부가적인 설명이기 때문에 궁금하신 분만 읽어보세용 😃

디지털 서명이기 때문에 신뢰가 보장되며 비밀키를 사용하면 HMAC 알고리즘을 통해 암호화되며 공개/개인키를 함께 사용할 때는 RSA나 ECDSA 알고리즘을 통해 암호화된다.
사인된 토큰은 클레임에 대한 무결성을 보장하며 토큰이 암호화되는 동안 다른 쪽에서는 클레임에 대한 정보를 알 수 없다. (클레임에 대한 정의는 payload 파트에 있어요!)

만약 공개/개인키로 서명되는 경우에는 개인키를 보유한 쪽이 사용자임을 인증한다.

JWT 언제 사용하나요?

1. 권한 인증

  • JWT의 가장 일반적인 사용 방법
  • 사용자가 로그인할 때, 각 요청의 후속 작업으로 JWT를 포함하는데, 이는 사용자가 라우트, 서비스, 리소스 등에 접근을 토큰을 통해 허가한다.
  • 요즘은 SSO(Single Sign On) 방법이 많이 쓰이고 있는데, 이유는 비용이 적고, 다양한 도메인에서 사용이 용이하기 때문이다.

2. 정보 교환

  • JWT는 서명이 가능하기 때문에 보낸 사람이 누구인지 확인이 가능한 장점이 있어 양측간의 보안된 정보를 전송하기 매우 좋은 방법이다.
  • 또한 signature은 header와 payload로 계산되기 때문에 콘텐츠가 조작되었는지 알 수 있다.

JWT 구조

  • JWT는 3가지의 요소를 포함하며 dots(.)으로 구분한다.
    • header
    • payload
    • signature
  • 3가지를 아래와 같이 표현한다.
{header}.{payload}.{signature}

header

  • header은 보통 2가지 요소를 포함한다. 예시는 아래와 같다.
  • JWT에서 Base64Url로 인코딩된다.
{
  "alg": "HS256",
  "typ": "JWT"
}
  • 위의 header를 해석하면 HS256 알고리즘을 사용하며 이것은 JWT인 것을 말한다.

payload

  • payload에서는 claims를 포함한다.
  • JWT에서 Base64Url로 인코딩된다.
  • claims는 일반적으로 사용자 정보를 포함하고, 그 외 정보들을 말한다.

claims의 분류

  1. Registered claims
    • 미리 정의된 claims set으로 필수 요소는 아니나 유용하고, 상호 운용 가능하게 제공되는 특징이 있어 권장 요소이다.
    • 이 claims의 종류의 예시는 아래외 같다.
      • iss(issuer, 발행자), exp(expiration time, 만료시간), sub(subject, 주제), aud(audience, 청자) an others
  2. Public claims
    • JWT를 사용하는 사람들이 자유롭게 정의할 수 있다.
    • 하지만 충돌을 피하기 위해 IANA JSON Web Tokin Registry를 통해 정의되거나 충돌 방지 네임스페이스를 포함한 URI를 통해 정의되어야 한다.
  3. Private claims
    • 정보를 공유하는 양측을 위한 커스텀 claims이다.
    • 공개적이거나 어딘가에 등록된 것은 아니다.

payload 구조

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

🚫 주의 🚫
header과 payload는 변조의 위험을 막아주긴 하지만 누구나 볼 수 있는 정보이기 때문에 내부에 민감한 정보를 포함하면 안된다.

signature

  • 앞서 Base64Url로 인코딩된 header과 payload를 secrey key와 함께 지정된 알고리즘으로 암호화한다.
  • 일반적으로 HMAC SHA256 algorithm를 사용한다.
  • 아래와 같은 형태이다.
const signature = HMACSHA256(
    {base64UrlEncode(header).
    base64UrlEncode(payload),
    secretKey)};

 

이렇게 Base64 Url로 인코딩된 3개의 요소를 dots(.)로 이으면 HTML과 HTTP 환경에서 쉽게 통과할 수 있다.

SAML과 같은 XML 표준과 비교할 때 더 간결해진다.

JWT 동작 방식

  • 인증시 자격 증명을 사용하여 성공적으로 로그인하면 JWT가 반환된다.
  • 토큰 자격 증명은 보안을 위해 필요 이상으로 길게 보관하면 안된다.
    • 말 즉슨, 만료 기간을 적당하게 설정해야 한다.
  • 그리고 너무 민감 정보를 담아서도 안된다.
  • 사용자가 어떤 리소스를 접근하려 할 때, Bearer 스키마를 활용하여 Authorization 헤더에 JWT를 담아 전송한다.
Authorization: Bearer {JWT}
  • HTTP header를 통해 JWT를 보내는 경우에는 토큰이 너무 커지지 않도록 한다.
    • 일반적으로 8kb를 넘지 않아야 한다.
  • Authorization에 토큰은 전송되면 쿠키를 사용하지 않기 때문에 CORS 에러는 발생하지 않는다.
  • API나 리소스에서 JWT를 어떻게 획득하는지 아래의 표에서 말한다.

1️⃣ 어플리케이션은 권한을 권한 서버로 요청한다. 이것은 다른 권한 플로우의 하나를 통해 수행한다. 예를 들어 일반적으로 OpenID Connect 호환 웹 어플리케이션은 /oauth/authorize 엔드포인트 권한 코드 플로우를 사용해 통과할 것이다.

2️⃣ 인증이 허가되면 권한 서버는 access token을 반환한다.

3️⃣ 어플리케이션은 보호된 리소스를 접근할 때(API처럼), access token을 사용한다.

JWT 만들어보기

JWT 생성은 어렵지 않다. 앞서 말한 JWT 구조를 순서대로 만들면 된다.

To do list

  1. base64 인코드 함수 만들기
  2. header base64로 인코드하기
  3. payload base64로 인코드하기
  4. 인코드된 header과 payload를 .으로 이어붙이기
  5. 4번에서 만든 것을 암호화하여 signature 만들기
  6. 2번, 3번, 5번을 .으로 이어붙이기
  7. 검증

1. base64 인코드 함수 만들기

운이 좋게도 Javascript 내장 메서드 중 base64로 만드는 함수는 존재한다.

btoa: 이진 데이터 문자열에서 Base64로 인코딩된 ASCII 문자열을 생성합니다("btoa"는 "binary to ASCII"로 읽어야 합니다).
atob: Base64로 인코딩된 문자열을 디코딩합니다("atob"는 "ASCII to binary"로 읽어야 합니다).

 

하지만 이 메서드를 사용해 텍스트를 인코딩하면 exception error 를 발생시킨다.
그 이유는 해당 메서드는 애초에 텍스트 인코딩 메서드가 아닌, 바이너리 인코딩 메서드이기 때문에 문자 용량을 초과했다는 에러가 발생된다. 바이트 코드 대응은 0x7f 까지의 코드 포인트에 대해서만 안정적으로 유지된다.

btoa를 통한텍스트 인코딩을 위해서는 다음과 같이 우회하여 사용한다.

1. 문자열 -> UTF-8의 바이트로 변환 ▶️ new TextEncoder() 사용

2. UTF-8 바이트를 바이너리 문자열로 변환 ▶️ Array.from(~)

3. 1번에서 변환한 것을 base64로 변환 ▶️ btoa() 사용

function objectToBytes(rawData) {
  const json = JSON.stringify(rawData); // object -> text
  const utf8Bytes = new TextEncoder().encode(json); // text -> utf-8 array
  const binString = Array.from(utf8Bytes, (e) => String.fromCodePoint(e)).join(''); // utf-8 array -> binary string
  return btoa(binString).replace('=', '');
}

objectToBase64("a Ā 𐀀 文 🦄");

 

=을 빈문자열로 대치시키는 이유는 base64 변환시 padding 문자(의미없는 문자열)이 섞여 있기 때문이다.
이를 제거하지 않으면 추후 JWT 해석시 warning으로 제거하라고 안내한다.

 

2. header를 base64로 변환하기

const HEADER = {
  alg: 'HS256',
  typ: 'JWT',
};

const encodedHeader = objectToBase64(HEADER);

 

3. payload를 base64로 변환하기

const payload = {
  ...userInfo,
};

const encodedPayload = objectToBase64(payload);

 

4. 인코드된 header과 payload를 .으로 이어붙이기

const signature = `${encodedHeader}.${encodedPayload}`

 

 

5. 4번에서 만든 것을 암호화하여 signature 만들기

header에서 나왔 듯 HMAC SHA256 알고리즘을 사용해서 signature을 생성할 것이다.
암호화를 하기 위해 crypto-js를 설치했다.

npm install crypto-js

 

secret key는 외부에 노출되면 안되기 때문에 환경 변수로 선언하여 불러왔다.

import crypto from 'crypto-js';

...

const sha256Signature = crypto.HmacSHA256(`${encodedHeader}.${encodedPayload}`, import.meta.env.VITE_SECRET_KEY);

 

암호화한 signature을 base64로 변환한다. 이때는 위의 만들어진 함수를 사용하지 않고, crypro에서 제공하는 메서드를 이용할 것이다.

왜냐하면 변환한 sha256Signature는 text형태도 아닌, crypto에서 제공되는 WordArray형태의 데이터이기 때문이다.

 

const base64Signature = crypto.enc.Base64url.stringify(sha256Signature).replace('=', '');

 

6. 2번, 3번, 5번을 .으로 이어붙이기

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

 

7. 검증

검증은 jwt.io에서 쉽게 검증 가능하다.

아래 사진과 같이 secret key를 먼저 입력하고, encoded 영역에 암호화된 JWT를 붙여넣는다.

 

 

Signature Verified가 표시되면 암호화에 성공한 것이다.

암호화에 실패하면 아래와 같이 Invalid Signature이 표시된다.

 

ref

괴롭다... 으어ㅓ...

728x90
반응형