Golang にて JWT認証を行う

Golang にて JWT 認証を行う必要があったので、メモ。

JWT 認証とは

JSON Web Token の略らしい。

JSON Web Token(ジェイソン・ウェブ・トークン)は、JSONデータに署名や暗号化を施す方法を定めたオープン標準 (RFC 7519) である。

とのこと。

仕組みの簡単な説明

送信する側は秘密鍵(シークレットキー的なもの)を用いて、データをトークン化する。
受け取る側も同じ秘密鍵を用いてトークンが正規なものか判断して扱う。

トークン化といえど BASE64 での変換ぽいので扱うデータは注意が必要。
見られてはまずい情報は扱わない。

golang での JWT 認証

JWT 認証を行うにあたって、
golang-jwt/jwt を使用した。

ドキュメントはこちら

トークンを発行する側

package main

import (
	"fmt"
	"github.com/golang-jwt/jwt/v5"
)

func main() {
	
	// 秘密鍵
	const SECRET_KRY = "SECRET_KRY"
	key := []byte(SECRET_KRY)
	
	// 送信したいデータ
	value := jwt.MapClaims{
		"key": "value",
		"test": "test value",
	}

	// トークンを生成
	t := jwt.NewWithClaims(jwt.SigningMethodHS256, value)
	token, err := t.SignedString(key)
	if err != nil {
		fmt.Print("\n== EEROR ==\n", err.Error(), "\n=====\n")
		return
	}

	// このトークン(token)を使う
	fmt.Print("\n== token ==\n", token, "\n=====\n")
}

トークンを受け取る側

受け取るであろうトークンを想定したデータは上記 token 参照。
下記、データの内容。

// 送信したいデータ
value := jwt.MapClaims{
	"key": "value",
	"test": "test value",
}

下記からサンプルコード。


package main

import (
	"fmt"
	"github.com/golang-jwt/jwt/v5"
)

func main() {

	// 受け取るであろうトークンを想定したもの
	const REQUEST_TOKEN = "REQUEST_TOKEN"

	// 秘密鍵
	const SECRET_KRY = "SECRET_KRY"
	key := []byte(SECRET_KRY)

	// トークンをパースする
	parsedToken, err := jwt.Parse(REQUEST_TOKEN, func(token *jwt.Token) (interface{}, error) {
		// ここら辺のコメントアウト部分はちゃんと調べてないのでよく分からない
		// if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
		// 	return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		// }
		
		// 秘密鍵はここで指定
		return key, nil
	})
	// 秘密鍵が正しくないとエラーが起きる
	if err != nil {
		fmt.Print("\n== EEROR ==\n", err.Error(), "\n=====\n")
		return
	}
	// parsedToken.Valid でも判定できる
	// 公式のサンプルコードは err でのチェックではなくこちらを採用している
	if !parsedToken.Valid {
		return
	}

	// データを扱う。 jwt.MapClaims に型変換しないと扱えない
	claims := parsedToken.Claims.(jwt.MapClaims)
	// 想定データは上記参照。 interface{} なので型変換が必要
	res, ok := claims["test"].(string)
	if !ok {
		return
	}

	// "test value"
	fmt.Print("\n== res ==\n", res, "\n=====\n")

	// for でも参照できる
	fmt.Print("\n== key: value ==\n")
	for key, value := range claims {
		fmt.Print(key, ": ", value, "\n")
	}
	fmt.Print("=====\n")
}

結局 JWT 認証の何がいいの?

検索すると「JWT 認証 使うな」や「JWT 使ってはいけない」などといった、
ネガティブなサジェストが出てくるけど、

結局こういうセキュリティ系の話って、
仕組みを理解しない限り何を使っても危ないと思うので、
正しく使いましょう。以外の感想が思い浮かばなかった。

まず良いか悪いか分からないけど、 JWT 認証はステートレス(状態をもたない)な状況を作れるらしい。
どういうことか、の説明でログイン機能がどの記事にも例が出される。

例えば、下記データをトークン化してフロントエンドに返す。

value := jwt.MapClaims{
	"userId": 1,
	"expireAt": 1714489200,
}

フロントエンドはこのデータをトークン化されたものを保存して、
以降このトークンをバックエンドに投げてログインを維持する。

といった内容なんだけど、
フロントエンド側が userId を保持しててバックエンドがその userId を信じて扱うという、
ちょっと危なそうな仕組み。に見える。

あまり無いとは思うけど、秘密鍵の内容がバレたら全てのユーザーの情報が抜かれる。

トークンを発行した側のバックエンドが、
トークンとそれに紐づいた userId の情報を DB やメモリキャッシュ等で保持していない。

これをステートレス(状態をもたない)という。
一応、バックエンド側でトークン情報を保存しなくて良いので、
ログイン維持機能の実装は大分楽になる。

次に使えそうな良さげな場面として、
サードパーティ間でのデータのやりとりに使う。とか。
今回この事案で、 JWT 認証を行ったのだけど、

お互いが共通の秘密鍵を共有して、
他の人に見られても特に問題なさそうなデータをやりとりする。

こういうのは向いてそう。