ginでユーザーの認証処理を行う　~jwtトークン認証編~

# ginでユーザーの認証を行う

# はじめに
この記事ではユーザーの認証処理のうちのフロントエンド側からAPIをリクエストされた時にバックエンド側でどのようにしてフロントエンド側にいるユーザーを認証するのかということを説明します。

:::note info
この認証ではjwtトークンを使用します。jwtトークン発行に関しての記事は[こちら](https://qiita.com/kenshin_8799/items/ab33ebf31d52df08fc8e)
:::
# 目次
1. そもそもjwtトークンって何？
1. jwtトークンを使用したAPIリクエスト時のユーザーの認証プロセスの概要
1. 実際のコード

# そもそもjwtトークンって何？
## JWTとは何か？
- jwtトークンとは一言で表すと**セキュアな情報交換を実現するためのデジタルトークン**です。
このjwtトークンを使用することで**クライアント側で今どのユーザーがログインしているかの判断**を行うことができたり、**そのユーザーのAPIアクセスの認可**を行うことができたりします。
また、jwtトークンは３つの部分から構成されます。
    1. **ヘッダー**：使用するアルゴリズムとトークンのタイプ（通常はjwt）を含みます。
    1. **ペイロード**：トークンに含まれるクレーム（情報）を含みます。例えばユーザーIDや有効期限など。
    1. **署名**：ヘッダーとペイロードを秘密鍵で署名したものです。これによってjwtトークン全体が暗号化されてトークンの改ざんを防げます。

:::note info
実際のjwtトークンは以下のようになっています。
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IlVzZXIxQGdtYWlsLmNvbSIsImV4cCI6MTczODEzOTI1Niwic3ViIjo4OX0.nllm4EckwZ8mOKlvxoo8aNb67PhYzlFFVbHoGXt3dDk
```
このトークンが先ほど伝えた**ヘッダー**と**ペイロード**と**署名**の3つの部分に分かれています。
:::

# jwtトークン発行のプロセスの概要
- 今回はjwtトークンを使用したAPIリクエスト時のユーザーの認証プロセスについてのみ解説します。本来はその前にjwtトークンを発行する必要があります。jwtトークン発行に関する記事は[こちら](https://qiita.com/kenshin_8799/items/ab33ebf31d52df08fc8e)から参照していただけます。

    jwtトークンを使用したAPIリクエスト時のユーザーの認証の全体の流れとしては下記のとおりです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3758326/edea7ef6-b6e8-3968-ab76-cefe2d23b43f.png)


# 実際のコード
## クライアント側からAPIのリクエストが送信される
- クライアント側からのAPI送信では主に下記のコードで処理を行います。
```typescript:page.tsx
        fetch('http://localhost:8080/Portfolio/getAllPosts', { credentials: 'include' })
            .then(res => res.json())
            .then(data => {
                setPortfolio(data.portfolio);
            })
            .catch(err => console.error(err));

```
- **HttpリクエストにCookieを含める**
    - 以下のコードでHttpリクエストにCookieを含めています。デフォルトでHttpリクエストにCookieが含まれるわけではないのでこの記述が必要になります。
```typescript:page.tsx
{ credentials: 'include' }
```

## Cookieを取得する
- Cookieの復号化では主に以下の処理を行います。
    1. Cookieからjwtトークンを取得する
    1. 秘密鍵を用いてjwtトークンを復号化する
```go:main.go
portfolioRouterWithAuth := r.Group("/Portfolio", middlewares.AuthMiddleware(authService))
portfolioRouterWithAuth.GET("/getAllPosts", portfolioController.GetAllPosts)

```
```go:auth_middleware.go
func AuthMiddleware(authService services.IAuthService) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		tokenString, err := ctx.Cookie("jwt-token")
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
			return
		}

		// トークンの検証とユーザーの取得
		user, err := authService.GetUserFromToken(tokenString)
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
			return
		}

		ctx.Set("user", user)

		ctx.Next()
	}
}
```

- **Cookieからjwtトークンを取得する**
    - まずはmain.goで**リクエストが特定のルートにアクセスを行う前に認証を行います**。
    - 以下のコードでPortfolioより下のルータに対して認証処理を行います。

```go:main.go
portfolioRouterWithAuth := r.Group("/Portfolio", middlewares.AuthMiddleware(authService))
```

そしてAuthMiddleware関数では**以下のコードでCokkieに含まれているjwt-tokenを取り出しています。**
```go:auth_middleware.go
tokenString, err := ctx.Cookie("jwt-token")
```

## 秘密鍵を用いてjwtトークンを復号化する
- 以下の２つの関数を使用してjwt-tokenの復号化を行います。
```go:auth_middleware.go
func AuthMiddleware(authService services.IAuthService) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		tokenString, err := ctx.Cookie("jwt-token")
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
			return
		}

		// トークンの検証とユーザーの取得
		user, err := authService.GetUserFromToken(tokenString)
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
			return
		}

		ctx.Set("user", user)

		ctx.Next()
	}
}
```
```go:auth_service.go
func (s *AuthService) GetUserFromToken(tokenString string) (*models.User, 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
	}
	var user *models.User
	if claims, ok := token.Claims.(jwt.MapClaims); ok {
		if float64(time.Now().Unix()) > claims["exp"].(float64) {
			return nil, jwt.ErrTokenExpired
		}

		user, err = s.repository.FindUserByEmail(claims["email"].(string))
		if err != nil {
			return nil, err
		}
	}
	return user, nil
}
```
- **取得したjwt-tokenをGetUserFromToken関数に渡す**
    - 以下のコードで処理を行います。これによって取得したjwt-tokenからそのユーザーを割り出せます。
    ```go:auth_middleware.go
    user, err := authService.GetUserFromToken(tokenString)
    ```

- **jwt-tokenを秘密鍵で復号化する**
    - 以下のコードで復号化を行います。ここでは署名方法を確認して秘密鍵を取得しています。今回の場合、署名方式は**SigningMethodHMAC**であり、秘密鍵は**SECRET_KEY**です。
    ```go:auth_service.go
    	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
	})
    ```

:::note info
**秘密鍵は何でもよいですが一般的には文字列で複雑で長い文字列であることが推奨されます。**
:::

:::note warn
この時点ではまだ復号化をしていません。秘密鍵を取得しただけです。
**復号化はjwt-tokenからメールアドレスを取得するときに同時に行います**。
:::

## 復号化されたjwt-tokenからメールアドレスを取得してユーザーを取得する
- この処理には以下の関数を使用します。（さっきのGetUserFromToken関数と同じもの）
```go:auth_service.go
func (s *AuthService) GetUserFromToken(tokenString string) (*models.User, 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
	}
	var user *models.User
	if claims, ok := token.Claims.(jwt.MapClaims); ok {
		if float64(time.Now().Unix()) > claims["exp"].(float64) {
			return nil, jwt.ErrTokenExpired
		}

		user, err = s.repository.FindUserByEmail(claims["email"].(string))
		if err != nil {
			return nil, err
		}
	}
	return user, nil
}
```
- **jwt-tokenのペイロードを解析する**
    - 以下のコードで処理を行います。ペイロードとは簡単に言ってしまえば、jwt-tokenの実際のデータもことです。
    - okがtrueの場合、トークンのペイロードが正しく取得されています。
```go:auth_service.go
if claims, ok := token.Claims.(jwt.MapClaims); ok {
```

- **有効期限の確認**
    - 以下のコードでペイロードの中から有効期限の値を取得して現在の時間と比較します。もし、有効期限が切れていたら処理を中断します。
```go:auth_service.go
if float64(time.Now().Unix()) > claims["exp"].(float64) {
    return nil, jwt.ErrTokenExpired
}
```

- **ユーザーの取得**
    - 以下のコードでこの処理を行います。ペイロードの中からemailを取得してその取得したメールアドレスに基づいてデータベースの中からユーザーを検索して返します。
    - ユーザーが見つからなかった場合やエラーが発生した場合は、そのエラーを返します。
```go:auth_service.go
user, err = s.repository.FindUserByEmail(claims["email"].(string))
if err != nil {
    return nil, err
}
```

## ctxにユーザー情報を保存して次の処理へつなげる
- この処理には以下の関数を使用します。（先ほど出てきたAuthMiddleware関数と同じ関数です）
```go:auth_middleware.go
func AuthMiddleware(authService services.IAuthService) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		tokenString, err := ctx.Cookie("jwt-token")
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
			return
		}

		// トークンの検証とユーザーの取得
		user, err := authService.GetUserFromToken(tokenString)
		if err != nil {
			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
			return
		}

		ctx.Set("user", user)

		ctx.Next()
	}
}
```

- **コンテキストにユーザー情報を保存する**
    - 以下のコードでこの処理を行います。コンテキスト（ctx）に「user」というキーでユーザー情報（user）を保存します。
    - **一度コンテキストに保存されたデータは、同じリクエストの他のミドルウェアやハンドラ関数でアクセスすることができます**。これにより、リクエスト全体を通じてデータを簡単に共有できるようになります。
```go:auth_middleware.go
ctx.Set("user", user)
```
- **次の処理へつなげる**
    - 以下のコードでこの処理を行います。**ctx.Next()が呼び出されると、現在のミドルウェアの処理が終了し、リクエストの処理が次のミドルウェアまたは最終的なルートハンドラに渡されます**。
    - 逆にこの処理がないと次のハンドラ関数へ移行できません。
    ```go:auth_middleware.go
    ctx.Next()
    ```
# まとめ
今回はjwtトークンによるユーザーの認証に関しての記事でした。[別の記事](https://qiita.com/kenshin_8799/items/ab33ebf31d52df08fc8e)でjwt-tokenの発行方法に関してもまとめているので是非ご一読ください。