Ginを利用して新規登録、ログイン機能をJWT認証で実装（JWT認証編）

# 概要

新規登録からログインまで解説してきて、いよいよ今回でJWT認証の実装を行なっていきます。

正直あまり参考になる記事がなく、自分なりにパッケージのドキュメントや海外の記事を参考にしました。

間違っている点もあるかもしれませんが、少しでも参考になればと思いアウトプットしていきます。

また、JWT認証についてよくわからない方は下記の記事で解説していますのでそちらを参照してください。

https://qiita.com/soicchi/items/b8c87a3f2f384b3d06ff

## 過去のシリーズ

https://qiita.com/soicchi/items/968568c839c182098a48

https://qiita.com/soicchi/items/56a0e900ca1000cee3e7

# 環境

[過去の記事](https://qiita.com/soicchi/items/968568c839c182098a48)にDockerの設定については記載しているのでそちらを参考にしてください。

なお、認証機能を構築するのに`jwt-go`パッケージを利用していきます。

バージョンは`v5.0.0-rc.1`を使用します。

# JWT認証実装

今回は**共通鍵認証**を前提とします。

処理を記載する前にまずは、`pkg`ディレクトリ配下に`token.go`を作成してください。

作成したファイルに処理を記載していきます。

## JWT生成

まずは`JWT`を生成する関数を作成していきます。

これは**新規登録時**や**ログイン時**にクライアントにレスポンスとしてトークンを返すために作成します。

```pkg/utils/token.go
package utils

import (
	"os"
	"strconv"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

func GenerateToken(userId uint) (string, error) {
	secretKey := os.Getenv("SECRET_KEY") // 暗号化、復号化するためのキー
	tokenLifeTime, err := strconv.Atoi(os.Getenv("TOKEN_LIFETIME"))
	if err != nil {
		return "", err
	}

	claims := jwt.MapClaims{
		"user_id": userId,
		"exp":     time.Now().Add(time.Hour * time.Duration(tokenLifeTime)).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(secretKey))
	if err != nil {
		return "", err
	}

	return tokenString, nil
}
```

`claims`には`user_id`を設定しました。

調べた中で`Password`や`email`を`claim`に保存している記事がありましたが、

個人的には**JWTに個人情報は含めるべきではない**と考えているので避けました。

なぜ個人情報を含めるべきではないというと、

JWTはあくまでBase64Urlというアルゴリズムでエンコードしたにすぎません。

もしJWTが流失してしまった場合に、デコードされるとJWTの中身が分かってしまします。

なので個人情報は含めるべきではないと考え、今回は`user_id`を格納しました!

`secretKey`は署名を暗号化・復号化するために必要です。

基本的に文字列ならなんでもいいですが、

私は秘密鍵などを生成する際によく使用される`openssl`コマンドを利用して作成しました。

```sh
openssl rand -base64 32
```

これはBASE64形式のランダムなデータを32バイト長で生成してくれます。

おそらく上記のコマンドを実行したら下記のような文字列がターミナルに出力されたのではないでしょうか。

```sh
0N3mCELJP4RvrKyigmq/Q0uB+ak3TGDuBRSSRKQvv9s=
```

ここで出力されたを値を各自設定してください。

また、トークンの有効期限も設定します。

今回`TOKEN_LIFETIME`という環境変数で設定していますが、各自好きなように設定してください。

トークンの有効期限のできるだけ短くしたほうが良いとされています。

なぜならトークンは**一度発行されると削除することが困難**なのため、数カ月単位などの長期期間は避けたほうがいいです。

## 新規登録処理を編集

前段ではトークンを生成する関数を作成しました。

今度は作成した関数を`controllers`の新規登録ハンドラーの中で呼び出したいので[以前の記事]()で作成した新規登録処理部分を編集していきます。

またクライアントとのトークンをやりとりは`Cookie`を利用します。

```/pkg/models/auth.go
func (handler *Handler) SignUpHandler(ctx *gin.Context) {
	var signUpInput models.SignUpInput
	err = ctx.ShouldBind(&signUpInput)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"message": "Invalid request body",
		})
		return
	}

	user := &models.User{
		Name:     signUpInput.Name,
		Email:    signUpInput.Email,
		Password: signUpInput.Password,
	}
	err = user.Validate()
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"message": err.Error(),
		})
		return
	}

	newUser, err := user.Create(handler.DB)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"message": "Failed to create user",
		})
		return
	}

    // 追加部分
	token, err := utils.GenerateToken(newUser.ID)
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"message": "Failed to sign up",
		})
		return
	}

    // Cookieにトークンをセット
	ctx.SetCookie("token", token, cookieMaxAge, "/", "localhost", false, true)
	ctx.JSON(http.StatusOK, gin.H{
		"user_id": newUser.ID,
		"message": "Successfully created user",
	})
}
```

先ほど作成した`GenerateToken`でトークンを生成し、

生成したトークンを`SetCookie`で`Cookie`にセットしています。

`SetCookie`メソッドについて各パラメータが何を指しているのか知りたい方は下記を参照してください。

`Gin`のソースコードを見ても各パラメーターが何を指しているのかよくわからなかったので下記を参考にしました！

https://enblog.yangyanxing.com/article/use-go-gin-cookie-session.html#:~:text=The%20first%20parameter,obtained%20by%20js

## JWTを検証する関数を生成

次にリクエストで送られてきたJWTを検証する関数を作成していきます。

```pkg/utils/token.go
package utils

import (
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

func GenerateToken(userId uint) (string, error) {
	secretKey := os.Getenv("SECRET_KEY")
	tokenLifeTime, err := strconv.Atoi(os.Getenv("TOKEN_LIFETIME"))
	if err != nil {
		return "", err
	}

	claims := jwt.MapClaims{
		"user_id": userId,
		"exp":     time.Now().Add(time.Hour * time.Duration(tokenLifeTime)).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(secretKey))
	if err != nil {
		return "", err
	}

	return tokenString, nil
}

// 下記追加部分
func ParseToken(tokenString string) (*jwt.Token, error) {
	token, err := jwt.Parse(tokenString, 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(os.Getenv("SECRET_KEY")), nil
	})
	if err != nil {
		return nil, err
	}
	return token, nil
}
```

`jwt.Parse`でトークンを検証し、不正なトークンの場合にエラーを返すようになっています。

# ミドルウェアを作成

次に`middleware`を作成していきます。

まず、`pkg`ディレクトリ配下に`middleware`ディレクトリを作成し、`middleware.go`ファイルを作ります。

具体的な処理は下記になります。

```pkg/middleware/middleware.go
package middleware

import (
	"net/http"

	"github.com/gin-gonic/gin"

	"app/pkg/utils"
)

func AuthMiddleware(ctx *gin.Context) {
	tokenString, err := ctx.Cookie("token")
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, gin.H{
			"message": "Unauthorized",
		})
		ctx.Abort()
		return
	}

	token, err := utils.ParseToken(tokenString)
	if err != nil {
		ctx.JSON(http.StatusUnauthorized, gin.H{
			"message": "Invalid token",
		})
		ctx.Abort()
		return
	}

	ctx.Next()
}
```

処理の流れとしては下記です。

1. リクエスト内のクッキーからJWTを取得
1. 取得したJWTを検証する
1. そして処理をHandlerに投げます

またトークンなどに不正があり認証できない場合は、後続のハンドラーに処理がいかないように

`ctx.Abort()`で処理をストップさせなければなりません。

これがないと、後続のハンドラーの処理が続いてしまうので必須です。

# ミドルウェアの設定

あとは認証が必要なエンドポイントごと設定すれば`middleware`を使用できます。

あくまで例ですが、

下記のように`middleware`を利用します。


```router.go
package router

import (
	"github.com/gin-gonic/gin"

	"app/pkg/controllers"
	"app/pkg/database"
    "app/pkg/middleware"
)

func Run() {
	router := setupRouter()
	router.Run()
}

func setupRouter() *gin.Engine {
	router := gin.Default()
	handler := &controllers.Handler{
		DB: database.GetDB(),
	}
	api := router.Group("/api")
	v1 := api.Group("/v1")
    users := v1.Group("/users")
    users.Use(middleware.AuthMiddleware) // middlewareを設定
    {
        users.GET("", handler.GetUsers) // ユーザー一覧を取得
    }

	return router
}

```

`users.Use(middleware.AuthMiddleware)`の部分でmiddlewareを設定しています。

上記設定であれば、`users`配下のエンドポイントにアクセスする際は`middleware`が作動します。

なので今後**認証した場合のみエンドポイントへのアクセスを許可**する場合は、上記のように追加していけばOKです。

# まとめ

いかがだったでしょうか。

**新規登録〜ログイン〜JWT認証**の3回に渡って解説してきました。

正直まだまだ勉強中なので未熟な部分もあるかと思います。

その場合は遠慮なくコメントいただけるとありがたいです！

なお、今回はログの管理やトークンからユーザーIDを抽出するような処理は記載しませんでしたが、

本来はあったほうがいいと思います。

実装する際はその辺も考えながら実装するといいかもしれません。

最後まで読んでいただいてありがとうございました！

# 参考

https://github.com/golang-jwt/jwt

https://pkg.go.dev/github.com/golang-jwt/jwt/v5#section-readme