JWT vs PASETO v2 vs TECTO: 2026년에 올바른 토큰 프로토콜 선택
Source: Dev.to
Tokens are everywhere in modern auth flows. But not all tokens are created equal.
In this post we compare three approaches side‑by‑side — classic JWT (HS256), the more modern PASETO v2, and the brand‑new TECTO — across security, ergonomics, and real‑code examples.
Quick Comparison Table
| Property | JWT (HS256) | PASETO v2 | TECTO |
|---|---|---|---|
| Payload visible? | ✅ 예 (base64) | ✅ 예 (signed, not encrypted) | ❌ 완전 암호화 |
| Cipher | 없음 (HMAC) | Ed25519 (sign) / XChaCha20 (encrypt) | XChaCha20‑Poly1305 |
| Nonce | N/A | 토큰당 24‑byte | 토큰당 24‑byte CSPRNG |
| Key size | 가변 | 가변 | 정확히 256‑bit (강제) |
| Tamper detection | HMAC 서명 | Ed25519 / Poly1305 태그 | Poly1305 인증 태그 |
| Error specificity | 이유 공개 | 이유 공개 | 일반 “Invalid token” |
| Algo‑confusion attacks | ⚠️ 예 (alg: none 문제) | ✅ 아니오 | ✅ 아니오 |
| Key rotation built‑in | ❌ DIY | ❌ DIY | ✅ 네이티브 (토큰에 kid 포함) |
1️⃣ JWT – The Old Faithful
jsonwebtoken은 Node.js에서 가장 널리 사용되는 토큰 라이브러리입니다. 검증된 안정성, 방대한 생태계, 그리고 시작하기 쉬운 간단함이 특징입니다.
npm install jsonwebtoken
import jwt from "jsonwebtoken";
const SECRET = "my-secret-key"; // ← 이것이 문제입니다
// Sign
const token = jwt.sign(
{ userId: 42, role: "admin" },
SECRET,
{ expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMCwiaXNzIjoibXktYXBwIn0.SIGNATURE
// Verify
const payload = jwt.verify(token, SECRET) as { userId: number; role: string };
console.log(payload.userId); // 42
Payload는 읽을 수 있다
// 任意의 JWT의 중간 부분을 디코드
const [, payload] = token.split(".");
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
console.log(decoded);
// {"userId":42,"role":"admin","iat":1700000000,"exp":1700003600,"iss":"my-app"}
토큰을 가로채는 사람은 키 없이도 페이로드를 읽을 수 있습니다. 이는 설계상 JWT가 암호화된 것이 아니라 서명된 것이기 때문이며, 많은 개발자들이 처음에 이를 인식하지 못합니다.
알고리즘 혼동 문제
JWT는 헤더에 알고리즘을 지정할 수 있습니다. 이 때문에 alg: none 공격이 유명해졌으며, 공격자는 알고리즘을 none으로 설정해 토큰을 위조할 수 있었습니다. 현대 라이브러리들이 none을 차단하더라도, HMAC vs RSA 혼동 공격은 여러 발행자로부터 토큰을 받아야 할 경우 여전히 실질적인 위험이 됩니다.
JWT가 적합한 경우
- 공개된 비민감 페이로드(예: 사용자 ID, 역할)
- JWT를 요구하는 서드파티 서비스와 연동(OAuth, OIDC 등)
- 팀이 이미 JWT 인프라를 보유하고 있는 경우
2️⃣ PASETO – 플랫폼에 구애받지 않는 보안 토큰
PASETO는 JWT의 함정을 해결하기 위해 설계되었습니다. 알고리즘 선택성을 완전히 없애고, 버전을 선택하면 고정된, 잘 선택된 알고리즘이 적용됩니다. alg: none도 없고, 혼동 공격도 없습니다.
npm install paseto
import { V2 } from "paseto";
const key = await V2.generateKey("local");
// ---- Encrypt (v2.local) ----
const token = await V2.encrypt(
{ userId: 42, role: "admin" },
key,
{ expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// v2.local.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// Decrypt
const payload = await V2.decrypt(token, key);
console.log(payload.userId); // 42
// ---- Sign (v2.public) ----
const { privateKey, publicKey } = await V2.generateKey("public");
// Sign (payload is visible, like JWT)
const signed = await V2.sign({ userId: 42 }, privateKey, { expiresIn: "1h" });
// Verify
const verified = await V2.verify(signed, publicKey);
주요 특징
- 알고리즘 혼동 없음 – 버전(
v2)이 알고리즘을 고정합니다. v2.local은 페이로드를 암호화합니다 (XChaCha20‑Poly1305).- 깔끔하고 현대적인 비동기 API.
- 키 회전 스토리가 없음 –
kid가 토큰 형식에 포함되지 않으므로, 키 버전 관리를 직접 해야 합니다. - 오류 메시지가 여전히 실패 원인을 드러낼 수 있습니다.
- 키에 대한 엔트로피 검증이 없으며, 약한 키를 전달해도 라이브러리가 조용히 받아들입니다.
Source: …
3️⃣ TECTO – Transport Encrypted Compact Token Object
TECTO는 다른 철학을 취합니다: 모든 토큰이 항상 완전히 암호화됩니다. “서명‑하지만‑읽을 수 있는” 모드는 없습니다.
bun add tecto
import {
generateSecureKey,
MemoryKeyStore,
TectoCoder,
InvalidSignatureError,
TokenExpiredError,
assertEntropy,
} from "tecto";
// 1️⃣ 암호학적으로 안전한 256‑bit 키 생성
const key = generateSecureKey();
// 2️⃣ 키 저장소 설정
const store = new MemoryKeyStore();
store.addKey("my-key-2026", key);
// 3️⃣ 코더 생성
const coder = new TectoCoder(store);
// 4️⃣ 암호화
const token = coder.encrypt(
{ userId: 42, role: "admin" },
{ expiresIn: "1h", issuer: "my-app" }
);
console.log(token);
// tecto.v1.my-key-2026.base64url_nonce.base64url_ciphertext
// 5️⃣ 복호화
const payload = coder.decrypt(token);
console.log(payload.userId); // 42
토큰 형식
tecto.v1...
- kid(키 ID)가 토큰 자체에 포함되어 있어 네이티브 키 회전이 가능하며, 별도의 메타데이터나 헤더가 필요 없습니다.
// 키 회전
store.addKey("key-2026-01", oldKey); // 오래된 키는 기존 토큰을 계속 복호화함
store.rotate("key-2026-06", newKey); // 새 토큰은 새 키를 사용함
store.removeKey("key-2026-01"); // 더 이상 필요 없을 때 오래된 키 삭제
강력한 키‑엔트로피 검사
// 모두 KeyError를 발생시킴
assertEntropy(new Uint8Array(32)); // 전부 0
assertEntropy(new Uint8Array(32).fill(0xaa)); // 반복 바이트
assertEntropy(new Uint8Array(16)); // 길이 오류
// 이것은 안전함
const goodKey = generateSecureKey(); // 항상 고엔트로피, 256‑bit
assertEntropy(goodKey); // ✅ 통과
일관된 오류 처리
try {
coder.decrypt(tamperedToken);
} catch (err) {
if (err instanceof InvalidSignatureError) {
// err.message === "Invalid token"
// 왜 실패했는지 알 수 없습니다 – 이것이 의도된 동작입니다.
// 공격자는 오류 메시지를 보고 시스템을 탐색할 수 없습니다.
}
if (err instanceof TokenExpiredError) {
// 토큰이 단순히 만료되었습니다 – 클라이언트에 알려도 안전합니다.
}
}
TECTO가 빛을 발하는 경우
- 모든 클레임에 대한 완전한 기밀성이 필요할 때.
- 별도 설정 없이 네이티브 키‑회전이 필요할 때.
- 탐색 공격을 방지하기 위한 일관적이고 비공개적인 오류 메시지가 필요할 때.
TL;DR
| 기능 | JWT (HS256) | PASETO v2 | TECTO |
|---|---|---|---|
| 페이로드 읽을 수 있나요? | ✅ (base64) | ✅ (서명됨) | ❌ (암호화됨) |
| 고정 알고리즘? | ❌ (alg header) | ✅ (버전 고정) | ✅ (내장) |
| 네이티브 키 회전? | ❌ | ❌ | ✅ (kid in token) |
| 강력한 키 크기 적용? | ❌ | ❌ | ✅ (256‑비트) |
| 일관된 오류 메시지? | ❌ | ❌ | ✅ |
| 최적 사용 사례… | 공개이며 민감하지 않은 데이터; 기존 JWT 생태계 | 서명된 토큰과 선택적 암호화를 원하는 최신 앱 | 기밀을 유지해야 하고 내장 회전 기능을 활용해야 하는 모든 경우 |
보안 및 운영 요구 사항에 맞는 토큰 형식을 선택하세요 – 그리고 ‘서명 = 안전’이라고 가정하지 마세요. 암호화도 중요합니다.
토큰 형식 비교
아래는 JWT, PASETO v2, 그리고 TECTO에 대한 간단한 비교입니다.
세 가지 모두 동일한 페이로드—예: { userId: 42, role: "admin" }—를 전달할 수 있지만, 데이터가 보호되는 방식은 크게 다릅니다.
구체적인 예시
// 전달하려는 페이로드
{ userId: 42, role: "admin" }
JWT (서명만)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4ifQ ← { userId: 42, role: "admin" }의 base64
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
누구든지 중간 세그먼트에 atob()을 실행해 페이로드를 읽을 수 있습니다 — 비밀 키가 필요하지 않습니다.
PASETO v2.local (암호화)
v2.local.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxFULL_CIPHERTEXT
XChaCha20‑Poly1305 로 암호화되었습니다. 키가 없으면 암호문은 불투명합니다.
TECTO (암호화 + 내장 키‑ID)
tecto.v1.my-key-2026.NONCE_BASE64URL.CIPHERTEXT_BASE64URL
XChaCha20‑Poly1305 로 암호화되었습니다.
kid(my‑key‑2026)는 라벨 형태로만 표시되며, 키 없이는 페이로드를 읽을 수 없습니다.
기능 매트릭스
| Feature | JWT | PASETO v2 | TECTO |
|---|---|---|---|
| Key ID in token | ❌ 표준이 아님 | ❌ 표준이 아님 | ✅ Built‑in kid |
| Old token decryptable after rotation | DIY (직접 구현) | DIY (직접 구현) | ✅ store.rotate()가 처리 |
| Revoke old key | DIY (직접 구현) | DIY (직접 구현) | ✅ store.removeKey()가 메모리를 0으로 초기화 |
| Entropy validation | ❌ 없음 | ❌ 없음 | ✅ assertEntropy() 적용 |
| Tamper‑evidence | ✅ HMAC | ❌ 페이로드가 누구에게나 읽히는 | ✅ Authenticated encryption (XChaCha20‑Poly1305) |
| Algorithm agility | ❌ 알고리즘 혼동 공격 표면 | ✅ 버전이 알고리즘 고정 | ✅ 버전이 알고리즘 고정 |
| Native key rotation | ❌ 없음 | ❌ 없음 | ✅ kid 사용 |
| Generic errors (no oracle attacks) | ✅ | ✅ | ✅ |
| Timing‑safe comparisons | ✅ | ✅ | ✅ |
| Memory zeroing on key removal | ✅ | ✅ | ✅ |
| Payload size limits (DoS prevention) | ✅ | ✅ | ✅ |
| Type‑checked registered claims | ✅ | ✅ | ✅ |
언제 어떤 토큰을 사용할까
-
Use JWT if you need:
- Compatibility with OAuth / OIDC / existing infrastructure
→ OAuth / OIDC / 기존 인프라와의 호환성 - Payloads that contain no sensitive data (just IDs, roles, etc.)
→ 페이로드에 민감한 데이터가 없고 (ID, 역할 등)만 포함될 때 - Integration with third‑party services that only understand JWT
→ JWT만 이해하는 서드파티 서비스와의 통합
- Compatibility with OAuth / OIDC / existing infrastructure
-
Use PASETO v2.local if you want:
- A well‑audited, standardized encrypted token
→ 잘 감사된 표준화된 암호화된 토큰 - Interoperability across many languages / platforms
→ 다양한 언어·플랫폼 간 상호 운용성 - No built‑in native key rotation (you’ll handle rotation yourself)
→ 기본 제공되는 네이티브 키 회전 기능이 없으며(직접 회전 관리)
- A well‑audited, standardized encrypted token
-
Use TECTO if you want:
- Encryption‑by‑default with zero configuration mistakes possible
→ 기본 암호화이며 설정 실수가 전혀 불가능 - Native key rotation without extra infrastructure
→ 별도 인프라 없이 네이티브 키 회전 - A greenfield TypeScript/Bun project
→ 새로운 TypeScript/Bun 프로젝트 - Defense‑in‑depth: entropy validation, generic errors, timing safety, memory zeroing, payload size limits, etc.
→ 방어 심화: 엔트로피 검증, 일반 오류, 타이밍 안전성, 메모리 영(0) 처리, 페이로드 크기 제한 등.
- Encryption‑by‑default with zero configuration mistakes possible
TECTO 빠른 시작
bun add tecto
import { generateSecureKey, MemoryKeyStore, TectoCoder } from "tecto";
// 1️⃣ 키 저장소를 생성하고 키를 추가합니다
const store = new MemoryKeyStore();
store.addKey("v1", generateSecureKey());
// 2️⃣ 저장소를 인식하는 코더를 만듭니다
const coder = new TectoCoder(store);
// 🔐 페이로드 암호화 (1시간 후 만료)
const token = coder.encrypt({ userId: 42 }, { expiresIn: "1h" });
// 🔓 복호화 (타입 검사됨)
const { userId } = coder.decrypt(token);
영구 키 저장소
TECTO는 SQLite, PostgreSQL, MariaDB용 어댑터를 제공하며, 모두 동일한 KeyStoreAdapter 인터페이스를 구현하므로 백엔드를 교체하는 것이 간단합니다.
요약
- JWT는 연합 인증 및 OAuth 흐름의 사실상 표준으로 남을 것이며, 이는 공개적이고 민감하지 않은 클레임에 대해서는 괜찮다.
- PASETO v2.local는 견고하고 표준화된 암호화 토큰을 제공하지만, 회전 및 엔트로피 관리를 직접 해야 한다.
- TECTO는 한 단계 더 나아간다: 배터리 포함 키 회전, 필수 엔트로피 검사, 일반 오류 처리, 타이밍‑안전 비교, 자동 메모리 제로화.
최고의 토큰 프로토콜은 잘못 구성할 수 없는 것이다.
TECTO가 그 점을 강력히 입증한다.