JWT(+JWS)について調べた

JWT とは

JSONオブジェクトで、安全に2者間で情報をやりとりするための表現方法です。 RFC7519で定義されています。

JWS とは

JSON Web Signatureの略、JWTのエンコード形式、デジタル署名、もしくはMACを行ったメッセージの表現 RFC7515で定義されています。 他には、暗号化するための仕様としてJSON Web Encryption (JWE)なんかがあります。

今回は、使われているケースが多いJWT+JWSの組み合わせのパターンでの実装を調べていきたいと思います。

特徴

  • コンパクトである
    • サイズが小さいので、URL、POSTパラメータ、またはHTTPヘッダー内で送信できます。
  • 自己完結されている
    • ペイロードには、ユーザーに関する必要な情報がすべて含まれているため、(使い方にもよりますが)データベースを複数回クエリする必要はありません。
    • ペイロードとは・・・やりとりしたい情報の本体(ヘッダなどの付加的情報が取り除かれたデータです。)
  • url-safeである
    • URLで使用できる文字列のみで構成されています。

どんなときに使うのか

  • 認証

    • こちらが一般的な使われ方のようです。
    • ユーザーがサービスにログインしJWTを発行してらもらい、その発行されたJWTで他に許可された異なるドメインのリソースを取得する。などの使い方ができます。
  • 情報の交換

    • JWTには、公開鍵/秘密鍵のペアを使用して署名することができるため、送信者は自分が誰であるかを確認できます。さらに、ヘッダーとペイロードを使用して署名が計算されるので、コンテンツが改ざんされていないことを確認することもできます。

OpenID ConnectやOAuthと組み合わせて使うケースもあるようです。

JWTの構造

これらをBase64エンコーディング(urlsafe、パディング無し)に変換し、.区切りで結合します。

こんな形になります。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE

ヘッダ

ヘッダには、このトークンのタイプ、使用されているハッシュアルゴリズムなどの情報で構成されます。

この例では、 ハッシュアルゴリズムが、HS256 (HMAC SHA-256アルゴリズムトークンのタイプが、JWT ということを示しています。

{
  "alg": "HS256",
  "typ": "JWT"
}

ペイロード

ペイロードは以下のような形式になります。 ペイロードにはクレームを含んでいます。 (クレームとは、エンティティ(通常はユーザー)と追加のメタデータのことを指します。)

クレームには、以下3種類があります。

  • 予約済み

    • 必須ではありませんが、事前定義された推奨のクレームです。例えば以下のようなものがあります。
      • iss(発行者)
      • exp(有効期限)
      • sub(サブジェクト)
      • aud(オーディエンス)
  • 公開済み

    • JWTを使用する人が自由に定義できます。 しかし、衝突を避けるためには、IANA JSON Webトークレジストリで定義するか、衝突抵抗ネームスペースを含むURIとして定義する必要があります。
  • 個人用

    • これらは、それらの使用に同意する当事者間で情報を共有するために作成されたカスタムクレームです。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "exp": 1540652400
}

有効期限(exp)は、 NumericDate である必要があるので、ここでは、UNIXTIMEを入れています。 https://tools.ietf.org/html/rfc7519#section-4.1.4

署名

先ほど、紹介したJOSE ヘッダ、JWSペイロード、(Base64urlエンコード済)を、 HMAC SHA-256 アルゴリズムで署名し, [JWS]に指定された形で署名をbase64urlエンコードすると, このエンコード済JWS署名を作り出せます。

RubyでJWTを作ってみる

# -*- coding: utf-8 -*-

require 'base64'
require 'json'
require 'openssl'

header = {
  alg: 'HS256',
  typ: 'JWT'
}

payload = {
  sub: '1234567890',
  name: 'John Doe',
  admin: true,
  exp: 1540652400
}

secret = 'secret'

encoded_header = Base64.urlsafe_encode64(header.to_json).delete('=')
encoded_payload = Base64.urlsafe_encode64(payload.to_json).delete('=')

signature = OpenSSL::HMAC.digest(
  OpenSSL::Digest::SHA256.new,
  secret,
  "#{encoded_header}.#{encoded_payload}"
)
encoded_signature = Base64.urlsafe_encode64(signature).delete('=')

jwt = [
  encoded_header,
  encoded_payload,
  encoded_signature,
].join('.')

puts '[jwt] => ' + jwt

結果

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE

値の確認

こちらのjwt.ioというサイトのDebuggerツールで、値の確認ができます。

https://jwt.io/#debugger-io

JWTの検証

こちらは、別の記事でまとめていく予定です。