LoginSignup
0
0

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

Posted at

こんにちは。

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

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

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

JwtBuilder

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

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の作成

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0