はじめに
Pythonでお馴染みになりつつある(と信じている)エンジニアの眞嶋です。
今回はOAuthやOpenIDほど複雑ではないですが、公開する上で一定の認証レベルを担保したい際に有効な手法としてJWT認証をご紹介します。
初の2部構成でJWT認証について実装まで解説していきます。
前編はJWTについて理解を深めるための記事なので、
「ワイ知っとるで!」という方は後編にお進みください。
そもそもJWTとは何ぞや
JWT認証と銘打った直後ですが、JWTというのは本来認証方式ではなく、認証の中で使用されるデータ形式のことを指しています。
「JSON Web Token」というのが正式名称で、その名前の通りトークンの形式として定義されたものになります。
構造としては、「ヘッダ」「ペイロード」「署名」の3つをつなぎ合わせ、最後に電子署名を付与した構造になっています。
実際にJWTを作ってみよう
ここまでは様々なところで紹介されている内容ですが、文字だけ見てもよくわからないと思います。(僕自身がそうでした)
ということでエンジニアらしく、ここは実際にコードを書いて解説していきます。
{
"alg": "HS256",
"typ": "JWT"
}
項目 | 値 |
---|---|
alg | 署名アルゴリズム HS256がメジャー |
typ | “JWT”固定 |
{
"iat": 1653199095,
"jti": "da5dd8a6-15c5-4197-9f6b-cc0f6051dcf2",
"type": "access",
"sub": "U0000000120",
"nbf": 1653199095,
"exp": 1653199995
}
項目(クレーム) | 値 |
---|---|
iat | JWTを発行した時刻 |
nbf | JWTが有効になる時刻 |
exp | JWTの有効期限 |
sub | 許可した対象の一意識別子 ユーザーIDなどが設定されるのが一般的 |
jti | JWTの一意識別子 |
これらをそれぞれBase64エンコードした文字列を「.(ドット)」繋ぎにすることで、署名なしトークンを作成します。
上記の値を使って実際に試してみましょう。
from base64 import urlsafe_b64encode
import json
def to_base64(data):
data_bytes = json.dumps(data).encode()
encoded = urlsafe_b64encode(data_bytes)
return encoded
header = {
"alg": "HS256",
"typ": "JWT",
}
payload = {
"iat": 1653199095,
"jti": "da5dd8a6-15c5-4197-9f6b-cc0f6051dcf2",
"type": "access",
"sub": "U0000000120",
"nbf": 1653199095,
"exp": 1653199995
}
header_base64 = to_base64(header)
payload_base64 = to_base64(payload)
print((header_base64 + b'.' + payload_base64).decode())
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpYXQiOiAxNjUzMTk5MDk1LCAianRpIjogImRhNWRkOGE2LTE1YzUtNDE5Ny05ZjZiLWNjMGY2MDUxZGNmMiIsICJ0eXBlIjogImFjY2VzcyIsICJzdWIiOiAiVTAwMDAwMDAxMjAiLCAibmJmIjogMTY1MzE5OTA5NSwgImV4cCI6IDE2NTMxOTk5OTV9
実はこれがJWTのデータ本体になります。
しかし、このままではこのデータが正しいものであることを保証できないので、認可トークンとして扱うにはセキュリティ的にリスクが高すぎて使えたものではありません。
このデータに対して電子署名を施すことで、サーバー側もクライアント側もこれが「正しく生成された」JWTであることを確認することができます。
from base64 import urlsafe_b64encode
import json
import hashlib
import hmac
def to_base64(data):
data_bytes = json.dumps(data).encode()
encoded = urlsafe_b64encode(data_bytes)
return encoded.decode()
header = {
"alg": "HS256",
"typ": "JWT",
}
payload = {
"iat": 1653199095,
"jti": "da5dd8a6-15c5-4197-9f6b-cc0f6051dcf2",
"type": "access",
"sub": "U0000000120",
"nbf": 1653199095,
"exp": 1653199995
}
header_base64 = to_base64(header)
payload_base64 = to_base64(payload)
secret_key = b'SecretKey'
no_signature = header_base64 + '.' + payload_base64
signature_bytes = urlsafe_b64encode(hmac.new(secret_key, no_signature.encode(), hashlib.sha256).digest())
signature = signature_bytes.decode().rstrip('=')
jwt = header_base64 + '.' + payload_base64 + '.' + signature
print(jwt)
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJpYXQiOiAxNjUzMTk5MDk1LCAianRpIjogImRhNWRkOGE2LTE1YzUtNDE5Ny05ZjZiLWNjMGY2MDUxZGNmMiIsICJ0eXBlIjogImFjY2VzcyIsICJzdWIiOiAiVTAwMDAwMDAxMjAiLCAibmJmIjogMTY1MzE5OTA5NSwgImV4cCI6IDE2NTMxOTk5OTV9.EyIdq6nIfBDQ4_dcA6airyBrIfafoQ6C-SuxFEHD2n4
これで電子署名を施したJWTトークンの完成です。
JWTの内容が正しいかを確認するには、「https://jwt.io/」が便利です。
JWT作るの面倒じゃない?
ここまで見てきた中で、JWTを作るのって結構手間がかかるんだなぁと感じた方もいることでしょう。
先ほどは構造を解説する目的でコードを書いたので長くなりましたが、ただ作るだけであればもっと簡単に作る方法もあります。
今回の趣旨と外れてしまうので割愛しますが、興味のある方は「PyJWT」と検索すると多くの記事が見つかりますのでご参考まで。
さいごに
本記事ではJWTについて理解を深めてもらおうと思い、ライブラリに頼らないJWT生成を行いました。
実装する際にはライブラリを使って、車輪の再発明をなるだけ減らしてもらう方がいいのですが、それに頼りすぎると「よくわからんが動いてるからヨシッ!」という某現場猫のようになってしまうので注意する必要がありますね。
さて、後編ではいよいよFlaskでの実装を行っていきますのでお楽しみに!
ちなみにFlaskではJWT生成と認証チェックまでまとめて対応してくれる便利なライブラリがあるので、それを使って実装していきます。(働き猫の話はどこいった!?)
コメント