【Go言語】JWT認証を実装してみる

目次

はじめに

Go言語でJWT認証を実装してみます。
本記事ではjwt-goライブラリを利用して、下記の内容を試します。

  • jwt-goによる認証情報の生成
  • jwt-goによる認証情報の検証
  • echoライブラリと連携した認証情報の検証

JWTとは

JWTとは

JWTとはJSON Web Tokenの略称で、JSON形式のデータに署名や暗号化を施す方法を定めた標準規格です。IETFによってRFC7519として標準化されています。
読み方はジェットと読むらしいです。

JWTは、JWS(JSON Web Signature)またはJWE(JSON Web Encryption)構造、あるいはその両方でエンコードされたJSONオブジェクトとして一連の情報(Claim)を表します。Claimとして、RFCで定義されている標準的なキーと値のペアを取ることにより、標準的な取り扱いが可能になります。Claimの標準的なキーと値については後述する構成要素の節に記載します。

このJWTは認証用のトークンとして使用されるケースがよくあります。次の図はJWTトークンを利用する一例です。

JWTトークンを利用する一例

Tokenの構成要素

JWT規格に沿って生成したTokenは次の三つの要素から構成されます。

  • ヘッダー
  • ペイロード
  • 署名

三つの構成要素を順番にピリオド. で繋ぎ、一つに値にしたものがTokenになります。

ヘッダー.ペイロード.署名

ヘッダー

ヘッダーには認証情報に関するメタ情報(主に署名生成に使用したアルゴリズム)がJSON形式で格納されます。Tokenで使用する際にはこのJSONをBase64urlエンコードします。

{
  "typ": "JWT"
  "alg": "HS256"
}

ヘッダーで使用される標準的なClaimは次の通りです。

コード名称説明
typToken typeトークンの形式。JWTとすることが推奨される。
ctyContent typeJWTを入れ子にして署名や暗号化を行う場合、このフィールドにJWTを指定する。それ以外では通常指定しない。
algMessage authentication code algorithm発行者が使用した署名アルゴリズム。任意のアルゴリズムが指定可能だが、いくつかのアルゴリズムは安全ではない。
参照:https://ja.wikipedia.org/wiki/JSON_Web_Token

ペイロード

ペイロードには認証情報が格納され、任意のClaimを格納することができます。JWTの仕様では、一般的に用いられる標準的なClaimが定義されています。

コード名称説明
issIssuerトークン発行者の識別子。
subSubjectトークンの主題の識別子。
audAudienceトークンが意図している受信者の識別子。トークンを受け付ける受信者は、この値に自身が含まれるかを識別しなければならない。もしaudクレームが存在し、かつ自身が含まれない場合、トークンを拒否しなければならない。
expExpiration Timeトークンの有効期限。この期限以降の場合、トークンを受け付けてはならない。有効期限は1970-01-01 00:00:00Zからの秒数を数値で指定する(UNIX時間)。
nbfNot Beforeトークンの開始日時。この期限以降の場合、トークンを受け付けてよい。秒数を数値で指定する。
iatIssued atトークンの発行日時。秒数を数値で指定する。
jtiJWT ID発行者ごとトークンごとに一意な識別子。
参照:https://ja.wikipedia.org/wiki/JSON_Web_Token

Tokenで使用する際にはJSONをBase64urlエンコードします。

{
 "loggedInAs" : "admin",
 "iat" : 1422779638
}

署名

署名はToken検証用の署名です。Base64urlエンコードしたヘッダーとペイロードをピリオドで繋ぎ、ヘッダーで指定したアルゴリズムで暗号化することで生成します。Tokenに使用する際には暗号化された値をさらにBase64urlエンコードします。

JWT認証の実装

jwt-goによる認証情報の生成

jwt-goライブラリには、一般的に使用される暗号化方式が次のように定義されています。

var (
	SigningMethodES256 *SigningMethodECDSA
	SigningMethodES384 *SigningMethodECDSA
	SigningMethodES512 *SigningMethodECDSA
)
var (
	SigningMethodHS256  *SigningMethodHMAC
	SigningMethodHS384  *SigningMethodHMAC
	SigningMethodHS512  *SigningMethodHMAC
	ErrSignatureInvalid = errors.New("signature is invalid")
)
var (
	SigningMethodRS256 *SigningMethodRSA
	SigningMethodRS384 *SigningMethodRSA
	SigningMethodRS512 *SigningMethodRSA
)
var (
	SigningMethodPS256 *SigningMethodRSAPSS
	SigningMethodPS384 *SigningMethodRSAPSS
	SigningMethodPS512 *SigningMethodRSAPSS
)

これら暗号化方式の定義はSigningMethod interfaceをimplementしており、暗号化に必要な情報及びメソッドを持っています。jwt-goでTokenオブジェクトを生成する際に、これら暗号化方式の定義を使用します。

// Implement SigningMethod to add new methods for signing or verifying tokens.
type SigningMethod interface {
	Verify(signingString, signature string, key interface{}) error // Returns nil if signature is valid
	Sign(signingString string, key interface{}) (string, error)    // Returns encoded signature or error
	Alg() string                                                   // returns the alg identifier for this method (example: 'HS256')
}

また、ペイロード情報はtype Claim interfaceに準拠するようtype MapClaims map[string]interface{}もしくはtype StandardClaims structを用いて作成します。

以上の暗号化方式の定義とペイロード情報を用いて、func NewWithClaims(method SigningMethod, claims Claims) *Tokenを実行することで、ヘッダーとClaimを含んだTokenオブジェクトを生成することができます。

Tokenオブジェクトを生成した後は、ユーザに返却する認証情報Tokenを生成して完了です。認証情報Tokenはfunc (t *Token) SignedString(key interface{}) (string, error)を実行することで生成できます。

package main

import (
	"fmt"

	"github.com/dgrijalva/jwt-go"
)

func main() {
	// Claimsオブジェクトの作成
	claims := jwt.MapClaims{
		"user_id": 12345678,
		"exp":     time.Now().Add(time.Hour * 24).Unix(),
	}

	// ヘッダーとペイロードの生成
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	fmt.Printf("Header: %#v\n", token.Header) // Header: map[string]interface {}{"alg":"HS256", "typ":"JWT"}
	fmt.Printf("Claims: %#v\n", token.Claims) // CClaims: jwt.MapClaims{"exp":1634051243, "user_id":12345678}

	// トークンに署名を付与
	tokenString, _ := token.SignedString([]byte("SECRET_KEY"))
	fmt.Println("tokenString:", tokenString) // tokenString: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzQwNTEzNjMsInVzZXJfaWQiOjEyMzQ1Njc4fQ.OooYrharapD5X2LV5UUWBOkEqH57wDfMd5ibkIpJHYM
}

jwt-goによる認証情報の検証

HTTPリクエストなどでトークンを受け取った後、正しいトークンかどうか検証を行います。次のように検証することができます。

  1. 受け取ったトークンからヘッダーとペイロードを取り出す
  2. 取り出したヘッダーとペイロードから改めて署名を作成
  3. 作成した署名と、トークンに含まれる署名を比較
  4. 一致していれば正しいトークン、不一致であれば正しくないトークン

jwt-goでこの検証を行うにはfunc (p Parser) Parse(tokenString string, keyFunc Keyfunc) (Token, error)メソッドを用います。
このメソッドは、第一引数に検証するトークン、第二引数に暗号化に用いたキーを検索するための関数を渡します。実行することでトークン文字列からTokenオブジェクトへとパースするとともに、内部的に検証を行い、Token.Validに検証結果を格納しまう。つまり、Token.Valid == true であれば正しいトークン、Token.Valid == falseであれば正しくないトークンであることがわかります。

package main

import (
	"fmt"

	"github.com/dgrijalva/jwt-go"
)

func main() {
	tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzQwNTEzNjMsInVzZXJfaWQiOjEyMzQ1Njc4fQ.OooYrharapD5X2LV5UUWBOkEqH57wDfMd5ibkIpJHYM"

	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// Don't forget to validate the alg is what you expect:
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}

		return []byte("SECRET_KEY"), nil
	})

	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		fmt.Printf("user_id: %v\n", int64(claims["user_id"].(float64)))
		fmt.Printf("exp: %v\n", int64(claims["exp"].(float64)))
	} else {
		fmt.Println(err)
	}
}

echoライブラリと連携した認証情報の検証

上述してきたjwt-goによる認証情報の生成と検証を、echoライブラリと連携して実現します。

echoにはJWT Middlewareが用意されています。echoのルーティング設定時にe.Use(middleware.JWTWithConfig(config))のようにJWT Middlewareを設定することで、そのルートを通るリクエストが届いた際にJWTトークンの検証を行ってくれるようになります。

検証する際に必要な情報はtype JWTConfigに設定します。今回は、トークン生成に使用した暗号化のキー(SigningKey)と、トークンのパースおよび解析をするParseTokenFuncをJWTConfigに渡します。

以上を踏まえて実装すると次のようになります。

package main

import (
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

// ログイン処理&トークン生成
func login(c echo.Context) error {
	username := c.FormValue("username")
	password := c.FormValue("password")

	// username, passwordの確認
	if username != "testname" || password != "testpw" {
		return echo.ErrUnauthorized
	}

	// ペイロードの作成
	claims := jwt.MapClaims{
		"user_id": 12345678,
		"exp":     time.Now().Add(time.Hour * 24).Unix(),
	}

	// トークン生成
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// トークンに署名を付与
	tokenString, err := token.SignedString([]byte("SECRET_KEY"))
	if err != nil {
		return err
	}

	return c.JSON(http.StatusOK, echo.Map{
		"token": tokenString,
	})
}

// ユーザ情報取得
func user(c echo.Context) error {
	user := c.Get("user").(*jwt.Token)
	claims := user.Claims.(jwt.MapClaims)
	userID := int64(claims["user_id"].(float64))
	return c.String(http.StatusOK, fmt.Sprintf("userID: %v", userID))
}

func main() {
	e := echo.New()

	// ログイン処理
	e.POST("/login", login)

	// user group
	r := e.Group("/user")

	// echo.middleware JWTConfigの設定
	config := middleware.JWTConfig{
		SigningKey: []byte("SECRET_KEY"),
		ParseTokenFunc: func(tokenString string, c echo.Context) (interface{}, error) {
			keyFunc := 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 []byte("SECRET_KEY"), nil
			}

			token, err := jwt.Parse(tokenString, keyFunc)
			if err != nil {
				return nil, err
			}
			if !token.Valid {
				return nil, errors.New("invalid token")
			}
			return token, nil
		},
	}
	r.Use(middleware.JWTWithConfig(config))
	r.GET("", user)

	e.Start(":1323")
}

正しく動作することを確認するため、リクエストを送ってみます。

▼トークン取得

% curl -X POST -F 'username=testname' -F 'password=testpw' http://localhost:1323/login 
// {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzQxNDI4NjIsInVzZXJfaWQiOjEyMzQ1Njc4fQ.kWMEchPsYOPiEECovoand4MYzWatfX8Bc4-nA25W4KE"}

▼取得したトークンを利用したユーザ情報取得

curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzQxNDI4NjIsInVzZXJfaWQiOjEyMzQ1Njc4fQ.kWMEchPsYOPiEECovoand4MYzWatfX8Bc4-nA25W4KE' http://localhost:1323/user
// userID: 12345678 

まとめ

本記事ではjwt-goを利用したJWT認証情報の生成、JWT認証情報の検証、およびechoライブラリと連携したJWT認証情報生成/検証の方法についてまとめました。認証周りは一見複雑なので難しい印象を持っていましたが、実際にコードを書いてみることで理解が深めることができました。

他のパッケージやGo言語における基本的な利用方法についても記事を書いていますので、もし興味がありましたらご参照ください。

【Go言語】echoフレームワークの使い方入門
【Go言語】ファイル/ディレクトリ操作方法 – 基本
【Go言語】database/sqlパッケージによるデータベース操作入門 – sqlite3
【Go言語】encoding/jsonパッケージでJSONをパースする
【Go言語】net/httpパッケージでAPIリクエストを送信する

よかったらシェアしてね!

コメント

コメントする

CAPTCHA


目次
閉じる