Go 言語で JWT を使って認証と認可を低コストに

  • 55
    Like
  • 0
    Comment
More than 1 year has passed since last update.

JWT とは

  • JSON Web Token のこと。
  • わかりやすい解説は JSON Web Token の効用 を、詳細は jwt.ioRFC 7519 を参照。
  • 簡単に言うと「認証するとトークンが発行されてそれ以降の認証認可をトークンで出来るよくあるあれ」。
  • ユーザーIDやアクセス権限をトークンに埋め込んでおくことで認可時にトークンの解析(復号化)だけでユーザー情報がわかるのでDBアクセスが必要な場合などに比べてコストが低くなる。
  • トークンには JSON であればなんでも埋め込めるという柔軟な点や、暗号鍵を共有するだけ(公開鍵暗号なら公開鍵のみ)で別のサーバーや別のシステムでも手軽に認可が可能であるというとりまわしが楽な点も魅力。
  • デメリットとして発行済みのトークンを有効期限内に失効させたい場合は別途検証する機構が必要になる。

Go 言語の JWT 実装

  • jwt.io には 4 種類載っていて、一番使われているっぽいのは github.com/dgrijalva/jwt-go

JWT をウェブサーバーで利用する

  • go-jwt-handler を使う。
  • JWT を使った認証ミドルウェアはすでにいくつかありましたが、一部の署名アルゴリズムにしか対応していなかったり、トークンの検証だけで発行をしてくれなかったり、特定の WAF に依存していたりとちょっと微妙だったので自分で実装してみた。
  • 特徴として、jwt-go で利用できる全ての署名アルゴリズムに対応し、トークンの発行と検証ができ、http.Request.Context を使ったトークンの引き渡しにより素の http.Handler として実装できている。

使用例

  • go-jwt-handler を使ったウェブサーバーの実装例。
  • / でログインフォームの表示、/login で認証とトークンの発行、/hello で認証が必要なコンテンツの返却例、/refresh でトークンの更新を行っている。
package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/nirasan/go-jwt-handler"
)

func main() {
    // ハンドラーの初期化
    jwth, err := jwthandler.New(jwthandler.Option{
        // 署名アルゴリズムの指定
        SigningAlgorithm: "HS256",
        // 署名アルゴリズムが HMAC SHA なので鍵の文字列を指定
        // RSA や ECDSA など公開鍵暗号の場合は秘密鍵と公開鍵のファイルのパスを指定する
        HmacKey:          []byte("MYKEY"),
        // 認証処理を定義する
        // 実際に使用する場合はここでデータベースなどを参照する想定
        Authenticator:    func(u, p string) bool { return u == "admin" && p == "admin" },
        // 認証処理に使うユーザー名とパスワードの取得処理を定義する
        LoginDataGetter:  func(r *http.Request) (string, string) { return r.FormValue("username"), r.FormValue("password") },
    })
    if err != nil {
        log.Fatal(err)
    }
    // ログインフォームの表示
    http.HandleFunc("/", index)
    // 認証とトークンの発行
    http.Handle("/login", jwth.AuthenticationHandler(http.HandlerFunc(login)))
    // 認証が必要なコンテンツの返却例
    http.Handle("/hello", jwth.AuthorizationHandler(http.HandlerFunc(hello)))
    // トークンの有効期限延長
    http.Handle("/refresh", jwth.TokenRefreshHandler(http.HandlerFunc(refresh)))
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func index(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, `
    <html>
        <head>
            <title>index</title>
        </head>
        <body>
            <form method="post" action="/login">
                <input type="text" name="username" />
                <input type="password" name="password" />
                <input type="submit" value="login" />
            </form>
        </body>
    </html>
    `)
}

// Input: curl -F 'username=admin' -F 'password=admin' http://localhost:8080/login
// Output: Your token is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NzM3MzEzNTQsInN1YiI6ImFkbWluIn0.zB6hoNjEHrcYhCx7KD_JdlauqTc08s_cB9IS7w49fyI
func login(w http.ResponseWriter, r *http.Request) {
    // 発行されたトークンをコンテキストから取得
    token, ok := jwthandler.SignedTokenFromContext(r.Context())
    if !ok {
        http.Error(w, "unauthorized", http.StatusUnauthorized)
    }
    fmt.Fprint(w, "Your token is "+token)
}

// Input: curl -H 'Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NzM3MzEzNTQsInN1YiI6ImFkbWluIn0.zB6hoNjEHrcYhCx7KD_JdlauqTc08s_cB9IS7w49fyI' http://localhost:8080/hello
// Output: Your name is admin
func hello(w http.ResponseWriter, r *http.Request) {
    // 認証に使われたトークンをコンテキストから取得
    token, ok := jwthandler.TokenFromContext(r.Context())
    if !ok {
        http.Error(w, "unauthorized", http.StatusUnauthorized)
    }
    // トークンからユーザーIDを取得
    username, ok := jwthandler.SubjectFromToken(token)
    if !ok {
        http.Error(w, "unauthorized", http.StatusUnauthorized)
    }
    fmt.Fprint(w, "Your name is "+username)
}

// Input: curl -H 'Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NzM3MzEzNTQsInN1YiI6ImFkbWluIn0.zB6hoNjEHrcYhCx7KD_JdlauqTc08s_cB9IS7w49fyI' http://localhost:8080/refresh
// Output: Your new token is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NzM3MzE0MDgsInN1YiI6ImFkbWluIn0.nPpJka3zzUdhVrK-hOV5tRYizmc82cmbfWRvmZNgWGo
func refresh(w http.ResponseWriter, r *http.Request) {
    // 再発行されたトークンをコンテキストから取得
    token, ok := jwthandler.SignedTokenFromContext(r.Context())
    if !ok {
        http.Error(w, "unauthorized", http.StatusUnauthorized)
    }
    fmt.Fprint(w, "Your new token is "+token)
}