タオルケット体操

サツバツいんたーねっと

JWTの使い方についての簡単な解説

当記事はJWTそのものではなく使い方に関する概要です。

JSON Web Token

以下のリンクが詳しいので、細かいところはそっちを参考にしてください。

JWT(JSON Web Token)とは、署名のできる、JSONを含んだURL Safeなトークン(Base64urlを使っている)のことです。
JSONなので任意の情報を含めることができます(一部、予約済みのフィールド名があります)。また、鍵による署名で内容の改ざんをチェックする機構もついています。
URLセーフなので取り回しもよいです。

JWTとJWSという名前が登場するかとおもいますが、JWSはJSON Web Signatureの略で、JSONにデジタル署名をするための規格の名前みたいです。

JWTの使い所

例えばWebアプリの場合、セッションの代わりに使えます。また、クッキーなどの機構を持たないアプリケーションでも認可の仕組みとして利用できるあたりが有用性になるんじゃあないでしょうか。

またuser_id(漏洩の可能性を考えると認証に使うIDは含めない方がいいとおもいます)などの情報や、expireなどの情報を保持させることができますし、scopeなどの情報もトークンに持たせることができるのでバックエンドの負担が減る(かもしれない)でしょう)。また、複数サーバでセッション情報を共有するためだけにRedisを導入するような必要もありません。

JWTのデメリット

標準化してまだ日が浅いせいか、(セッション代わりに使う場合の)チュートリアルやフレームワーク側のサポートはあまり手厚くありません。
幸い仕組み自体はシンプルで理解しやすいものですし、ほとんどの言語で便利なライブラリが実装されているのでそれを組み込んでいけば良いです。

また色々な情報を持たせることができるので、credentialとして使う場合には余計な情報を含めてしまわないような注意が必要かもしれません(これはJWT自体の問題ではないかもしれませんけど)。

JWTを使う上での注意点

JWT + JWSで作成したトークンは、あくまで改竄防止の署名が入っているBase64トークンなので簡単に解読できます。
なのでJWTのペイロードには重要なデータ(パスワードとか)を挿入しないようにしましょう。

先ほどuser_idを含めるなど……という話をしましたけども、もしかしたらサーバ内部の構造に関わるような値は露出しないようにしたほうがいいかもしれませんね。
tokenテーブルのようなものを作って、そのidにuserのidを紐ずけるような雰囲気の設計がベターなのかもしれません。

JWE(JSON Web Encryption)という規格を組み合わせて暗号化することもできるみたいなのですが、JWEがまだdraft段階(?)なせいか、まだほとんど実装が存在しません(調べた限りでは)。
まぁウェッブケー的な使い方をする分にはJWEなしでも問題ないとおもいます。

Go言語による実装例

まず適当な方法で鍵ペアを生成します。

Go言語による秘密鍵と公開鍵の生成

せっかくなのでGoを使います。が、別に何で生成しても良いでしょう。

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "log"
    "os"
)

func main() {
    priv, err := rsa.GenerateKey(rand.Reader, 2048)
    privDer := x509.MarshalPKCS1PrivateKey(priv)
    if err != nil {
        log.Fatal(err)
    }
    pubkey := priv.Public()
    pubDer, err := x509.MarshalPKIXPublicKey(pubkey)
    if err != nil {
        log.Fatal(err)
    }

    pemblock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: privDer}
    pubblock := &pem.Block{Type: "RSA PUBLIC KEY", Bytes: pubDer}
    privFile, err := os.OpenFile("./testkey_rsa", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
    defer privFile.Close()
    pubFile, err := os.OpenFile("./testkey_rsa.pub", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
    defer pubFile.Close()
    if err != nil {
        log.Fatal(err)
    }
    pem.Encode(privFile, pemblock)
    pem.Encode(pubFile, pubblock)
}

あんまり自信がないですが、これでtestkey_rsatestkey_rsa.pubが生成されたはずです。

JWTトークンの生成、解読

では肝心のJWTトークンの生成と解読です。
以下では jwt-go を利用します。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "time"
    "github.com/dgrijalva/jwt-go"
)

var (
    defaultPrivKey, _ = ioutil.ReadFile("./testkey_rsa")
    defaultPubKey, _  = ioutil.ReadFile("./testkey_rsa.pub")
)

func genJWT() *jwt.Token {
    token := jwt.New(jwt.SigningMethodRS512)
    token.Claims["iss"] = "testest"
    expire := time.Hour * 800
    token.Claims["exp"] = time.Now().Add(expire).Unix()
    return token
}

func parseTokenTxt(tokenTxt string) (*jwt.Token, error) {
    return jwt.Parse(tokenTxt, func(tk *jwt.Token) (interface{}, error) {
        // アルゴリズムタイプの検証を忘れないように!(README.mdより)
        if _, ok := tk.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", tk.Header["alg"])
        }
        return defaultPubKey, nil
    })
}

func main() {
    // JWTの生成
    token := genJWT()
    // JWTに署名する
    tokenTxt, err := token.SignedString(defaultPrivKey)
    if err != nil {
        log.Fatal(err)
    }

    // クライアントなどから帰ってきたJWTを解析する処理
    parsedToken, err := parseTokenTxt(tokenTxt)
    if err != nil {
        fmt.Println("an error occured")
        log.Fatal(err)
    }
    // 中身は平文なのでこの時点で "testest" という文面は確認できる
    fmt.Println(parsedToken.Claims["iss"])
    // ここで署名の正当性を確認できる
    if parsedToken.Valid {
        fmt.Println("your token is valid", parsedToken.Valid)
        fmt.Printf("%+v", parsedToken)
    } else {
        fmt.Println("your token is not valid", parsedToken.Valid)
        fmt.Printf("%+v", parsedToken)
    }
}

Webアプリケーションの認可の仕組みとして使う場合は、まず生成、署名、トークンテキストの生成を行います。次にクライアントから送られてきたトークンを解析し、Validで改竄がないか確認したのちにpayloadに含まれたidentityに基づいた処理を行う……というようになるでしょう。

jwt-goは微妙にdocがはしょられていたり、肝心なところで型がinterface{}だったりするせいで微妙に(かなり)使い方がわかりにくいんですが、その時は上記の参考URLや jwt_test.go を読めばだいたい使い方がわかるので頑張りましょう。

まとめ

というわけで、JWTなんぞやという説明は大きく端折ってほぼ「jwt-goをどう使うか」みたいな舵取りのエントリになりました。
色々な言語で実装がありますが、どれもだいたい似たような感じだとおもいます。
Objective-CやJavaScriptだとParseにだけ特化したライブラリなんかも多いですね。

まだがっつりと使えている状況ではないですが、かなり便利っぽいのでこれからトークンを発行するときはバンバン使っていこうかなとおもいました。

おしまい。


Amazon Web Services実践入門 (WEB+DB PRESS plus)

Amazon Web Services実践入門 (WEB+DB PRESS plus)

  • 作者: 舘岡守,今井智明,永淵恭子,間瀬哲也,三浦悟,柳瀬任章
  • 出版社/メーカー: 技術評論社
  • 発売日: 2015/11/02
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る