4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby開発Advent Calendar 2022

Day 7

JWTを実装して理解する

Last updated at Posted at 2022-12-06

JWTとは

  • JSON Web Tokenです。
  • 発音は"ジョット"らしい。
  • 認証用などで用いられています。

JWTの構成

JWTは、3つの部分から構成されます。

  • ヘッダー(header): JWTのアルゴリズムとトークンの種類を示します。
  • ペイロード(payload): トークンに含める情報(ユーザー名やIDなど)を示します。
  • 署名(signature):ヘッダーとペイロードから作成されるハッシュです。この署名を検証することで、トークンが改ざんされていないことを確認することができます。

3つの部分が「.」で区切られ、1つの文字列としてトークンが表されます。
<header>.<payload>.<signature>
JWTの例:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJEVndOWUV6SmxMRnNfWUVYZmhTWiIsInN1YiI6IjZ4Y2hvLnNlcnZpY2VhY2NvdW50QGJhbWJpLTUiLCJpYXQiOjE2NTI4ODUyNjEsImV4cCI6MTY1Mjg4ODg2MX0.V3tJaDy6wmM71UOXjpj09NxZnz6_Vma3nIYdaXc5VX-O9I10myUGIb0aYrhZ58zHN_MaxXUf901tMU8VVGhQlwx0NWu1Gb536j4Qnb3sGxi1WYd6V49ekhV8-hDsKlCKh-XM9y8ceDOIcpiSixO_AmAyolMTqlo04Zl8bjEQa_3BrSm9zo5D4TZXvPxmMT9OQGzRKsF860CBcWBey8R1E0a2jb77_cwpfLmSmeMu1KdfuINXeavxIaNqa7_D7uR3v_WNOn7OpTVi2uDRtJWLvngxBYz5AozP7rCyGflsvn_5xYqCTe4ybvCGfQeWAlgdKkNy5yqmMjtgxAF9QT-ZxQ

分かりやすくする為にピリオドごと改行します

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJpc3MiOiJEVndOWUV6SmxMRnNfWUVYZmhTWiIsInN1YiI6IjZ4Y2hvLnNlcnZpY2VhY2NvdW50QGJhbWJpLTUiLCJpYXQiOjE2NTI4ODUyNjEsImV4cCI6MTY1Mjg4ODg2MX0
.
V3tJaDy6wmM71UOXjpj09NxZnz6_Vma3nIYdaXc5VX-O9I10myUGIb0aYrhZ58zHN_MaxXUf901tMU8VVGhQlwx0NWu1Gb536j4Qnb3sGxi

JWTの構成の各部分を見ていきましょう!

ヘッダー(header)

JWTのヘッダーは、JWTのアルゴリズムとトークンの種類を示すものです。JWTのアルゴリズムには、暗号化や署名に使用する技術を指定します。一般的に使用されるアルゴリズムは、HMAC(ハッシュ・メッセージ認証・コード)やRSA(署名技術)です。また、トークンの種類を示す「typ」フィールドもあります。

JWTのヘッダーは、JSON形式で表され、以下のようなものとなります。

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

上記の例では、「alg」フィールドでRSAアルゴリズムの「RS256」が指定されています。また、「typ」フィールドでトークンの種類が「JWT」であることが示されています。このヘッダーは、Base64Urlエンコードされて、トークンの最初の部分として使用されます。例えば、上記の例のヘッダーをBase64Urlエンコードすると

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9

となります。

ペイロード(payload)

JWTのペイロードは、トークンに含める情報(ユーザー名やIDなど)を示すものです。JWTのペイロードには、標準的なフィールドが用意されています。

  • iss(Issuer):トークンの発行者
  • sub(Subject):トークンの主題(ユーザーIDなど)
  • aud(Audience):トークンの受信者
  • exp(Expiration Time):トークンの有効期限
  • iat(Issued At):トークンの発行日時
  • jti(JWT ID):トークンの一意の識別子

また、ペイロードには、独自のフィールドを追加することもできます。例えば、ユーザーの情報(名前やメールアドレスなど)や、アプリケーション固有の情報を含めることができます。

JWTのペイロードは、JSON形式で表され、以下のようなものとなります。

{
  "iss": "DVwNYEzJlLFs_YEXfhSZ",
  "sub": "6xcho.serviceaccount@bambi-5",
  "iat": 1670342714965,
  "exp": 1670342718565
}

上記の例では、「iss」フィールドにトークンの発行者、「sub」フィールドにユーザーID、「iat」フィールドにトークンの発行日時、「exp」フィールドにトークンの有効期限(iat+3600、つまり1時間)が含まれています。このペイロードは、Base64Urlエンコードされて、トークンの真ん中の部分として使用されます。例えば、上記の例のペイロードをBase64Urlエンコードすると、

eyJpc3MiOiJEVndOWUV6SmxMRnNfWUVYZmhTWiIsInN1YiI6IjZ4Y2hvLnNlcnZpY2VhY2NvdW50QGJhbWJpLTUiLCJpYXQiOjE2NzAzNDI3MTQ5NjUsImV4cCI6MTY3MDM0MjcxODU2NX0

となります。

署名(signature)

JWTの署名は、JWT内に含まれる情報が本物であり、改ざんされていないことを確認するために使用されるデジタル署名のことです。署名は、秘密鍵を使用して生成されるJWTのエンコードされたヘッダーとペイロードから作成されるハッシュです。これにより、JWT内の情報が改ざんされていないことが保証され、信頼できるソースから来たことが確認されます。

例えば上記のヘッダーとペイロードをピリオド.で結合して得た文字列は以下となります。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJEVndOWUV6SmxMRnNfWUVYZmhTWiIsInN1YiI6IjZ4Y2hvLnNlcnZpY2VhY2NvdW50QGJhbWJpLTUiLCJpYXQiOjE2NzAzNDI3MTQ5NjUsImV4cCI6MTY3MDM0MjcxODU2NX0

この文字列を以下の秘密鍵で暗号し、

-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCb/Zpn1eP+ti/Y
0mQwRJDwOX5o1Q/QgLvQ1Zd0dAijrpBJnJ5sUslnsnhqL8NsmdCgFiEkqmZ+EPZB
SEoV9r6aTXs+RxTCErFTscNqHh4y9d2X1EIGoThjW1od4wpCH9Jh4s2d7uDI5gix
MexVI1rH/C4kz5eTbM/S4Cs8OYldkccdwnBkJdAFrN6PDNjie8vozzxhCGr1e4ik
JcBj1uksnGqkWCn/A8CNjafUVwcDu6ZQkoqmYaT4FS+E0LIaOQ0JSB/afOqJHgfi
yX4N/X8Atrrr1CGoxSBuKh1ROMs0ZUEmqvwRancu+O8EPvpXV0uPlsFqXE2W2nSM
MknuAZbFAgMBAAECggEAfwkdQgrSzpCYjr9okFaGotxvsfgyFN+CFSSdX11P291B
7HD8I8OX+jdCEVgskrkBSh47h5yuM2Sk561ziEeWGhuXjrRcl2CZwUs3lahJd777
Y7acvsF1JpUy4rwPkqJPtx/5y/DKZ1HPKl851GwidlNNUPUuwAURiu68xyMkV2oj
0trKLzi9kEp3Lire0Uxua0HDvKtIlnFn3e7MVhAH2mFh+KMpxSCiJpm5/OrcXdQ7
iEGWKwc+shX0aOfNmi/jsd/egZJ70wVhOnUwZohSyswODx8MRtEcgGkBfv/XWGgV
YY3aoYAzXFaNf6yC2BUzhlVQrV+R3mRIFowFn+J9AQKBgQDoLFqeKqMqT3eMcDsC
AeyljpPbEIgTnIVOzenvS0fIV0wnoxivazCOISpmweacU7rYyc9aYLKQpXvl/UKS
LNYqq7VgkMRQZVeQimJJgqJB3Qvo0nrw1gQyNeNb9ducRJlPnw3y/aG64wnCzofP
K9PiBzLvoTzqQEITF+qFAILblQKBgQCr/8hXZ6OMNV+g7w5Aa0c49ueZbA6oV8gr
ERVz8X6Uper+8S8LQiNl9f2uIdxVFKaGl9UsabB9qsZhuogWl+LJ881qnONXjopC
UNUNFHTsMorBsuiXJyWGx8MNhpCNbVCWaDqIfsV4QpnEP7iTihm8tOqz0sC6Pelf
WXBARomCcQKBgHNH2rLgXuYaUTTlYRdNF1DZK6dBXR/HvxVy/u43hev/FKw1uoMX
iSN5alFdhAEpPgR4qEGq898InC2FhDGKm8jSO5w4JZH5z/HQFJ+FgsWLWWeAY1UE
H09DHKJDwIimMy2qv0VhFJX0N5uH6z09+NYpVuxPNizmPSi33lWt/EvFAoGAS3a0
HbU5CoE4bdG0/QqUeyfJQYUpgS7SgiLCuNYR5YJOcoC7VUM1aLzfCpGMrD+dOJxR
SWUtrFVLKQHAR7loJZc0XzxqerXtb18rusto6WDbhFTMu9TPIC1xJPQPZB28J+Gf
HNgSJ8uAz5vtjb4fwK3X9AHZ0VzskYsaNvk7x+ECgYBNnvgCbAPqDWGg5Ea1rgcB
sQ/rZr/ZyT/UKnm9VobycrdmE/OgITYnlTrRf6lR0LMk8+UwjthwiFODJcjDV93e
U7ZSYY32qLNpmEAFF2sobrpUbR0lQSaiXHzfD/KvXbqaLViWSbRVsNQvqKsOMzuK
vSgyXVol9a3MGFOMWLiuFg==
-----END PRIVATE KEY-----

Base64Urlエンコードすると、

BelxRKHW76KJVZ0x0GY_T7rEdNFAj97sYSXslipgIewcfM5E8XWbG3lLVq9wGZ-3vtNVzXx1MgRH0yaz-IiHMB7Z9ReNtr0_SnS8QsfGG8YfxknMfTcvv4RW8Bzp53sACeUuIOlsQ_4Yc3ciS_ydqnyAGSOleWetRdguyClqTfYKXFB2ziVDZty5jeaXnIIao-87a2vFX_3al9HCebzqyoO4aHGWFwu33QZj_KaHvz1S08wxlvOo5NSSXKyz7RSMp4ufkBw4vZ7UrEBX2qWcqT1mSEMV99_fw8ErCprkChtUm-vAANmflZyzDFx2A3HlXdCTOJFQ7ZeQ4m0MeK2_eg

となります。

Rubyで実装してみます

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

header =
{
  alg:"RS256",
  typ:"JWT"
}

payload =
{
  iss: "DVwNYEzJlLFs_YEXfhSZ",
  sub: "6xcho.serviceaccount@bambi-5",
  iat: Time.now.to_i,
  exp: Time.now.to_i + 3600
}

# ヘッダとペイロードをエンコードする
jwt_header = Base64.urlsafe_encode64(header.to_json, padding: false)
jwt_payload = Base64.urlsafe_encode64(payload.to_json, padding: false)

# 秘密鍵を読み込む
PRIVATE_KEY = File.read("/Users/caobi/Downloads/private_20220518105211.key")
# RSA秘密鍵を作る
RSA_PRIVATE_KEY = OpenSSL::PKey::RSA.new(PRIVATE_KEY)

# 署名を作る
signature = jwt_header + "." + jwt_payload
# 署名を暗号してBase64でエンコードする
jwt_signature = Base64.urlsafe_encode64(RSA_PRIVATE_KEY.sign(OpenSSL::Digest::SHA256.new, signature), padding: false)

# 最終的なJWTを作る
jwt = jwt_header + "." + jwt_payload + "." + jwt_signature
#"jwt: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJEVndOWUV6SmxMRnNfWUVYZmhTWiIsInN1YiI6IjZ4Y2hvLnNlcnZpY2VhY2NvdW50QGJhbWJpLTUiLCJpYXQiOjE2NTI4ODUyNjEsImV4cCI6MTY1Mjg4ODg2MX0.V3tJaDy6wmM71UOXjpj09NxZnz6_Vma3nIYdaXc5VX-O9I10myUGIb0aYrhZ58zHN_MaxXUf901tMU8VVGhQlwx0NWu1Gb536j4Qnb3sGxi1WYd6V49ekhV8-hDsKlCKh-XM9y8ceDOIcpiSixO_AmAyolMTqlo04Zl8bjEQa_3BrSm9zo5D4TZXvPxmMT9OQGzRKsF860CBcWBey8R1E0a2jb77_cwpfLmSmeMu1KdfuINXeavxIaNqa7_D7uR3v_WNOn7OpTVi2uDRtJWLvngxBYz5AozP7rCyGflsvn_5xYqCTe4ybvCGfQeWAlgdKkNy5yqmMjtgxAF9QT-ZxQ"
p "jwt: #{jwt}"

# jwtライブラリを使って検証する
require 'jwt'
token = JWT.encode(payload, RSA_PRIVATE_KEY, 'RS256', header)
#"tkn: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJEVndOWUV6SmxMRnNfWUVYZmhTWiIsInN1YiI6IjZ4Y2hvLnNlcnZpY2VhY2NvdW50QGJhbWJpLTUiLCJpYXQiOjE2NTI4ODUyNjEsImV4cCI6MTY1Mjg4ODg2MX0.V3tJaDy6wmM71UOXjpj09NxZnz6_Vma3nIYdaXc5VX-O9I10myUGIb0aYrhZ58zHN_MaxXUf901tMU8VVGhQlwx0NWu1Gb536j4Qnb3sGxi1WYd6V49ekhV8-hDsKlCKh-XM9y8ceDOIcpiSixO_AmAyolMTqlo04Zl8bjEQa_3BrSm9zo5D4TZXvPxmMT9OQGzRKsF860CBcWBey8R1E0a2jb77_cwpfLmSmeMu1KdfuINXeavxIaNqa7_D7uR3v_WNOn7OpTVi2uDRtJWLvngxBYz5AozP7rCyGflsvn_5xYqCTe4ybvCGfQeWAlgdKkNy5yqmMjtgxAF9QT-ZxQ"
p "tkn: #{token}"
# true
p jwt == token


参考文献:
https://jwt.io/

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?