なぜやるの?
JWTを最近触っていてわかっていることは,改ざんチェックができるJSONくらいの認識しかない
JWTを触る上で少しでも理解していないといざというときに危ないと考えるため
どうやるの?
golangライブラリのjwt-goで作成したJWTをpythonで作り直す
ここでの縛りとしてpythonのライブラリは
- JWTとは?
- JWTのシグネチャ部分の構造
- jwt-goで作成してみる
- pythonでパースしてみる
- pythonでjwtを作成する
JWTとは?
以前書いた記事を見ていただきたい
JWTのシグネチャ部分の構造
JWTのシグネチャ部分は以下のように生成される
ハッシュ関数(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
ハッシュ関数の第一引数にbase64UrlEncode(header) + "." +base64UrlEncode(payload)を,第二引数に秘密の鍵を渡してあげると生成することができる
jwt-goで作成してみる
main.go
package main
import (
"fmt"
"github.com/dgrijalva/jwt-go"
)
type jwtCustomClaims struct {
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.StandardClaims
}
func main() {
claims := &jwtCustomClaims{
"test",
true,
jwt.StandardClaims{
ExpiresAt: 1539762311, //有効期限をを発行しておく
},
}
j := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Secretで文字列にする. このSecretはサーバだけが知っている
token, _ := j.SignedString([]byte("secret")) //学習用なのでerr処理は飛ばす
fmt.Println(token)
}
ここではNameをtest,adminをtrueにして鍵をsecretにしている
ExpiresAt: 1539762311
に関して,本来は現在の時刻を取得してあげればいいはず
今回は固定しておく
そうするとgo runの結果は以下のようになると思う
$ go run main.go
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9.qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o
何度か実行してみて常に同じ値が出されることを確認する
pythonでパースしてみる
jwt.ioでパースしてもいいが今回は自力でパースする
main.py
import sys,re
import base64
import json
import hashlib,hmac
import urllib.request
args = sys.argv
str = args[1].split('.')
print(args[1])
header = base64.b64decode(str[0])
payload = base64.b64decode(str[1])
print(header)
print(payload)
実行の仕方は
$ python main.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9.qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o
長い.....ので
$ python main.py $(go run main.go)
上も下も実行結果は同じになるはずだ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9.qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o
b'{"alg":"HS256","typ":"JWT"}'
b'{"name":"test","admin":true,"exp":1539762311}'
1行目はmain.pyを実行するときに引数に与えた文字列がそのまま表示されている
2行目はヘッダ部分をbase64でデコードした値
3行目も同様にペイロード部分をbase64でデコードした値になる
ここでわかることは....
webアプリケーションでjwtを使うとき少なくとも
ヘッダ,ペイロードは見ることができるということだ
pythonでjwtを作成する
ここまででわかっていることは
- 秘密の鍵 ->
secret
- ヘッダ ->
{"alg":"HS256","typ":"JWT"}
- ペイロード ->
{"name":"test","admin":true,"exp":1539762311}
- jwt -> 下に書いた
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9.qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o
ここからシグネチャ部分を生成していく
全体のコードはこうなる
main.py
import sys,re
import base64
import json
import hashlib,hmac
import urllib.request
args = sys.argv
str = args[1].split('.')
print(args[1])
header = base64.b64decode(str[0])
payload = base64.b64decode(str[1])
print(header)
print(payload)
hp = base64.urlsafe_b64encode(header)+b'.'+base64.urlsafe_b64encode(payload)
print(hp)
secret = b'secret'
signature = hmac.new(secret,hp, hashlib.sha256)
signature = signature.digest()
signature = base64.urlsafe_b64encode(signature)
jwt = (hp+b"."+signature).decode("UTF-8").strip("=")
print(jwt)
以下は詳細を表記しているが,その前にもう一度どのようにシグネチャ部分が生成されるか表記する
ハッシュ関数(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
まずはbase64UrlEncode(header) + "." +base64UrlEncode(payload)
を求める
hp = base64.urlsafe_b64encode(header)+b'.'+base64.urlsafe_b64encode(payload)
print(hp)
変数hederには先程わかっているところで示したヘッダが入り,同様にpayloadにはペイロードが入る
実行結果は以下のようになるだろう
b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9'
次に第二引数と変数hpを使いシグネチャを作成する
secret = b'secret'
signature = hmac.new(secret,hp, hashlib.sha256)
signature = signature.digest()
signature = base64.urlsafe_b64encode(signature)
print(signature)
実行結果は以下のようになるだろう
b'qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o='
ここでほぼ一致していることがわかる
次に作成したものを結合していく
jwt = (hp+b"."+signature).decode("UTF-8").strip("=")
print(jwt)
.
で結合していることがわかる
ついでに文字列にしてbase64のパディングを削除している
そうすると実行結果は以下のようになるだろう
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9.qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o
これを先程の結果と比べると一致していることがわかるだろう
まとめ
ここでもう一度全体のコードを表記する
まずはディレクトリ構成
.
|-- main.go
`-- main.py
のようになっている
次にgolangでjwt-goにてjwtの発行を行ったmain.go
main.go
package main
import (
"fmt"
"github.com/dgrijalva/jwt-go"
)
type jwtCustomClaims struct {
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.StandardClaims
}
func main() {
claims := &jwtCustomClaims{
"test",
true,
jwt.StandardClaims{
ExpiresAt: 1539762311, //有効期限をを発行しておく
},
}
j := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Secretで文字列にする. このSecretはサーバだけが知っている
token, _ := j.SignedString([]byte("secret")) //学習用なのでerr処理は飛ばす
fmt.Print(token)
}
実行するとjwtが発行される
$ go run main.go
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9.qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o
次に自力でjwtを発行したmain.py
main.py
import sys,re
import base64
import json
import hashlib,hmac
import urllib.request
args = sys.argv
str = args[1].split('.')
print(args[1])
header = base64.b64decode(str[0])
payload = base64.b64decode(str[1])
print(header)
print(payload)
hp = base64.urlsafe_b64encode(header)+b'.'+base64.urlsafe_b64encode(payload)
print(hp)
secret = b'secret'
signature = hmac.new(secret,hp, hashlib.sha256)
signature = signature.digest()
signature = base64.urlsafe_b64encode(signature)
jwt = (hp+b"."+signature).decode("UTF-8").strip("=")
print(jwt)
おすすめする実行の仕方は
$ python main.py $(go run main.go)
そうすると
実行結果は
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9.qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o
b'{"alg":"HS256","typ":"JWT"}'
b'{"name":"test","admin":true,"exp":1539762311}'
b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9'
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImFkbWluIjp0cnVlLCJleHAiOjE1Mzk3NjIzMTF9.qKfKFVcMSfM2_3qy01YUmOr4tO6uzDLNU4w-yXFye7o
1行目はmain.goで発行したもの
5行目はpythonで作成したもの
一致していることがわかる
ありがとうございました