[JWT] JSON Web Token 指南

JWT | JWS | JWE | JWK

Intro

JWT (JSON Web Token) 是一種基於 JSON 的開放標準(RFC 7519),它定義了一種簡潔(compact)且自包含(self-contained)的方式,用於在雙方之間安全地將訊息作為 JSON 物件傳輸。而這個訊息是經過數位簽章(Digital Signature),因此可以被驗證及信任。可以使用 密碼(經過 HMAC 演算法) 或用一對 公鑰/私鑰(經過 RSA 或 ECDSA 演算法) 來對 JWT 進行簽章。

Wiki - JSON Web Token

  • RFC 7515 - JSON Web Signature(JWS)
    JWT 的簽名標準,包括 Header、Payload 和 Signature,Header 指定了所使用的算法和相關的參數。
  • RFC 7516 - JSON Web Encryption(JWE)
    加密 JWT Payload 的標準,包括 Header、Public key、IV、Ciphertext 和 Tag,Header 指定了所使用的加密算法和相關的參數。
  • RFC 7517 - JSON Web Key(JWK)
    用於描述加密和簽章金鑰的 JSON 格式,可以表示對稱式或非對稱式加密、簽章的金鑰。
  • RFC 7518 - JSON Web Algorithms(JWA)
    定義用於 JWS 和 JWE 中的加密、解密和簽名算法的標準
  • RFC 7519 - JSON Web Token(JWT)
    JWT 定義為 JWS 或 JWE 的 Compact Serialization 表現形式,本質上只是 JWS 或 JWE 的一種封裝方式。(實務上,提到 JWT 通常是指 JWS)

應用情境

  • 授權 - Client-Server Authorization
    這是 JWT(JWS)最常見的應用方式之一。例如使用者在 Client 端成功登入後,Server 會簽發一組 JWT。之後使用者每次對 Server 發送請求時,會附上這個 JWT(通常放在 Authorization: Bearer header 中)。Server 則使用預先設定的 secret key 驗證簽章,從而確認使用者身分與授權。
    此方式可達成無需儲存使用者狀態的 Stateless 架構(無需使用 session),Server 端可以僅需用一組 key 簽發並驗證所有 JWT。
    此外,JWT 也常被應用於單一登入(Single Sign-On, SSO),其優勢在於實作成本低,且特別適合跨網域(cross-domain)授權場景。

JWT Bytebytego
(Reference: ByteByteGoHq/system-design-101 - Github)

  • 驗證 - Server-to-Server Authorization
    在 Server 之間進行授權或資料交換時,可以使用 JWT(JWS)作為驗證機制。雙方事先約定一組共同的 secret key(對稱式加密),用於對 JWT 進行簽章與驗證,以確保傳送資料的完整性與來源可信性。
    實作上,可以將 API 規格中的主要 body 資料作為 JWT 的 payload 進行簽章以利接收方驗證,JWT 可以直接定義在 request body 中。

  • 訊息交換 (Information Exchange)
    JWT(JWS)可用於雙方之間的訊息交換,其簽章機制能確保訊息的完整性與來源可靠性。簽章可使用對稱式金鑰(如 HS256)或非對稱式的公私鑰對(如 RS256)。
    JWE(JSON Web Encryption)則應用於保障訊息內容的機密性,透過資料加密傳輸來確保訊息的安全性。


資料結構與格式

JWT

根據定義,JWT 僅是 JWS 或 JWE 的 Compact Serialization 表現形式。
然而,在實務上,提到 JWT 通常預設是指 JWS,例如多數線上工具預設僅支援簽章驗證而非解密。

JWT - Debugger Tool

傳送交換格式:

JWTs are always represented using the JWS Compact Serialization or the JWE Compact Serialization.
JSON Web Token (JWT) RFC-7519

JWS

JWS Compact Serialization:

BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload) || '.' ||
BASE64URL(JWS Signature)

Example code:

header = { "alg": "HS256", "typ": "JWT" }
payload = { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
signature = HMAC-SHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret)

JWS = base64UrlEncode(header) + '.' + base64UrlEncode(payload) + '.' + base64UrlEncode(signature)

JWS JSON Serialization - General & Flattened:

適用於有多組 signatures,或希望以結構化 JSON 方式傳遞簽章資料。

{
  "payload": "base64urlPayload",
  "signatures": [
    {
      "protected": "base64urlProtectedHeader",
      "signature": "base64urlSignature"
    },
    {
      "protected": "base64urlAnotherHeader",
      "signature": "base64urlAnotherSignature"
    }
  ]
}

JWE

JWE Compact Serialization

BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)

Example code:

header = base64UrlEncode({ "alg": "RSA-OAEP", "enc": "A256GCM", "kid": "5saf8g" })
encryptedSecretKey = RSA-OAEP(publicKey, contentEncryptionKey) // CEK (Content Encryption Key)
iv = generateRandomIV()
cipherText, tag = AES-256-GCM_Encrypt(plaintext, contentEncryptionKey, iv, aad = header)

JWE = base64UrlEncode(header) + '.' +
      base64UrlEncode(encryptedSecretKey) + '.' +
      base64UrlEncode(iv) + '.' +
      base64UrlEncode(cipherText) + '.' +
      base64UrlEncode(tag)

JWE JSON Serialization

適用於有多組 recipients,或希望以結構化 JSON 方式傳遞加密資料。

{
  "protected": "base64urlProtectedHeader",
  "recipients": [
    {
      "header": { "alg": "RSA-OAEP", "kid": "1234" },
      "encrypted_key": "base64urlEncryptedKey1"
    },
    {
      "header": { "alg": "RSA-OAEP", "kid": "5678" },
      "encrypted_key": "base64urlEncryptedKey2"
    }
  ],
  "iv": "base64urlIV",
  "ciphertext": "base64urlCiphertext",
  "tag": "base64urlTag"
}

指定 Direct Encryption (alg: "dir")

alg: "dir" 時,表示使用 Direct Encryption 模式。這意味著雙方直接使用事先約定好的對稱式金鑰進行加解密,而不需要額外產生並加密一組對稱式金鑰來進行訊息加密與交換。

根據 RFC 7516 規範,在此模式下的 JWE Compact Serialization 應如下

BASE64URL(Protected Header) || '.' ||
"" || '.' ||  // Empty Encrypted Key
BASE64URL(IV) || '.' ||
BASE64URL(Ciphertext) || '.' ||
BASE64URL(Authentication Tag)

When Direct Key Agreement or Direct Encryption are employed, let the JWE Encrypted Key be the empty octet sequence.
RFC7516 - 5.1. Message Encryption

JWE JSON Serialization - General & Flattened


JWK - JSON Web Key

JWK(JSON Web Key)用於表示加密金鑰的結構與格式,至於實際使用的加密演算法,則由 JWA(JSON Web Algorithms)所規範。

Key 結構格式

以下以 LINE Developer 文件中的 JWK 為例說明:

private.key example:

{
  "alg": "RS256",
  "d": "GaDzOmc4......",
  "dp": "WAByrYmh......",
  "dq": "WLwjYun0......",
  "e": "AQ......",
  "ext": true,
  "key_ops": [
    "sign"
  ],
  "kty": "RSA",
  "n": "vsbOUoFA......",
  "p": "5QJitCu9......",
  "q": "1ULfGui5......",
  "qi": "2cK4apee......"
}

public.key example:

{
  "alg": "RS256",
  "e": "AQ......",
  "ext": true,
  "key_ops": [
    "verify"
  ],
  "kty": "RSA",
  "n": "vsbOUoFA......"
}

JWK 應用情境

在 LINE Message API 中,Issue channel access token v2.1 的流程會使用到 JWK 格式:

  1. Developer 自行產生一組 Key Pair(公開金鑰與私密金鑰)。
  2. 將公開金鑰(Public Key)以 JWK 格式上傳到 LINE Developer Console,綁定至對應的 Channel。註冊成功後,LINE 會為該組公開金鑰產生一組 kid(Key ID)。
  3. 之後,當 Developer 要取得 Channel Access Token 時,使用該組私密金鑰(Private Key)來簽章 payload 產生一個 JWT(實際為 JWS)。並在 JWT Header 中指定 kid,讓 LINE Server 知道要用哪一組 Public Key 驗章。
  4. LINE Server 驗證該 JWS 簽章正確後,即回傳 Channel Access Token。


SDK 開發套件

PHP

Python

JAVA

NodeJS


References

Leave a Reply

Your email address will not be published. Required fields are marked *