【Go】ログイン機能でウェブアプリを作ってみる（9）

こんにちは。

Part 9は「ログイン JwtBuilder」です。

# 今回の目標

- Jwtを作成・解析するauthパッケージの作成

# 鍵の準備

今回のJWTではRS256を採用します。

### RS256？
- WTの暗号化（？）形式
- HS256
    - 署名化と検証を同じ鍵で行う
- RS256
    - 秘密鍵で署名化し、公開鍵で検証する
    - この公開鍵でOKってことは、ちゃんと僕の秘密鍵で作成されたJWTだな！」
    - 「xxxさんの公開鍵でOKってことは、このJWTはxxxさんが作成したものに違いない！」

### 鍵の作成

```
$ openssl genrsa 2048 > secret.pem
$ openssl rsa -pubout < secret.pem > public.pem
```

この２つの鍵はauth/keysディレクトリを作成し、そこに移動させてください。

これで鍵の準備はOKです。

# JwtBuilder

次にJWTを扱うためのパッケージを準備します。

https://github.com/lestrrat-go/jwx

auth/jwt_builder.go
```
package auth

import (
	_ "embed"
	"errors"
	"fmt"
	"login-go/entity"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/lestrrat-go/jwx/v2/jwa"
	"github.com/lestrrat-go/jwx/v2/jwk"
	"github.com/lestrrat-go/jwx/v2/jwt"
)

var (
	//go:embed keys/secret.pem
	secretKey []byte
	//go:embed keys/public.pem
	publicKey []byte
	
	// アクセストークンの有効期限
	expAccess = 30 * time.Minute
)

const (
	userIDClaim      = "user_id"
	issClaim         = "login-go"
	accessSubClaim   = "access-token"
	userIDContextKey = "user_id"
)

type IJwtGenerator interface {
	GenerateToken(u *entity.User) ([]byte, error)
}

type IJwtParser interface {
	SetAuthToContext(c echo.Context) error
}

type JwtBuilder struct {
	secretKey jwk.Key
	publicKey jwk.Key
}

func NewJwtBuilder() (*JwtBuilder, error) {
	secKey, err := jwk.ParseKey(secretKey, jwk.WithPEM(true))
	if err != nil {
		return nil, fmt.Errorf("failed to parse JWK: %w", err)
	}
	pubKey, err := jwk.ParseKey(publicKey, jwk.WithPEM(true))
	if err != nil {
		return nil, fmt.Errorf("failed to parse JWK: %w", err)
	}

	j := &JwtBuilder{}
	j.secretKey = secKey
	j.publicKey = pubKey
	return j, nil
}

// JWTを作成する
func (j *JwtBuilder) GenerateToken(u *entity.User) ([]byte, error) {
	// JWTを作成
	tok, err := jwt.NewBuilder().
		Issuer(issClaim).
		Subject(accessSubClaim).
		IssuedAt(time.Now()).
		Expiration(time.Now().Add(expAccess)).
		Claim(userIDClaim, u.ID).
		Build()
	if err != nil {
		return nil, fmt.Errorf("failed to jwt build: %w", err)
	}

	// JWTを秘密鍵で署名化
	signed, err := jwt.Sign(tok, jwt.WithKey(jwa.RS256, j.secretKey))
	if err != nil {
		return nil, fmt.Errorf("failed to sign: %w", err)
	}
	return signed, nil
}

// contextに認証情報をセットする
func (j *JwtBuilder) SetAuthToContext(c echo.Context) error {
	// リクエストからJWTを取得＆検証
	tok, err := j.parseRequest(c.Request())
	if err != nil {
		return err
	}

	// JWTからuser_idを取得する
	// idの型はtokenから取得した段階ではfloat64
	id, ok := tok.Get(userIDClaim)
	if !ok {
		return errors.New("failed to get user_id from token")
	}
	uid, ok := id.(float64)
	if !ok {
		return fmt.Errorf("get invalid user_id: %v, %T", id, id)
	}

	// ContextにUserIDをセットする
	c.Set(userIDContextKey, entity.UserID(uid))

	return nil
}

func GetUserIDFromEchoCtx(c echo.Context) (entity.UserID, error) {
	got := c.Get(userIDContextKey)
	uid, ok := got.(entity.UserID)
	if !ok {
		return 0, fmt.Errorf("get invalid user_id: %v, %T", got, got)
	}

	return uid, nil
}

// リクエストからJWTの取得し、検証を行う
func (j *JwtBuilder) parseRequest(r *http.Request) (jwt.Token, error) {
	// AuthorizationヘッダーからJWTを取得
	// 公開鍵を用いてjwtを検証、issとsubも検証する
	tok, err := jwt.ParseRequest(r,
		jwt.WithKey(jwa.RS256, j.publicKey),
		jwt.WithIssuer(issClaim),
		jwt.WithSubject(accessSubClaim),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to parse request: %w", err)
	}
	return tok, nil
}
```

### 関係ない話。自分がJWTでやった失敗
```
// リクエストからJWTの取得し、検証を行う
func (j *JwtBuilder) parseRequest(r *http.Request) (jwt.Token, error) {
	// AuthorizationヘッダーからJWTを取得
	// 公開鍵を用いてjwtを検証、issとsubも検証する
	tok, err := jwt.ParseRequest(r,
		jwt.WithKey(jwa.RS256, j.publicKey),
		jwt.WithIssuer(issClaim),
		jwt.WithSubject(accessSubClaim),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to parse request: %w", err)
	}
	return tok, nil
}
```

ここでJWTのIssがlogin-goか、Subがaccess-tokenか、を検証してます。
なんで検証してるんでしょうか？公開鍵で検証してるんだからissとsubの検証は必要ないと思いませんか？
自分はそう考えて、以前公開鍵のみで検証するようにしてました。（ちゃんと期限とかの検証はしてましたよ！）

ここでちょっとした失敗をしました。
どんな失敗だと思いますか？
ちなみにその時は期限1時間の期限が短いアクセストークンと、期限1週間の期限が長いリフレッシュトークンを使ってました

- アクセストークンもリフレッシュトークンも同じ秘密鍵で作成してる
- アクセストークンはsubがaccess-token
- リフレッシュトークンはsubがrefresh-token
- でもsubの検証はしてない
- はい、リフレッシュトークンをアクセストークンとして使えますね。
- 期限が長いアクセストークンの完成です！

# まとめ

今回やったこと
- authパッケージの作成
- JwtBuilderの作成

今日は以上です。
お疲れ様でした。次回はログイン処理を書いていきましょう。

ありがとうございました！
よろしくお願いいたします。