LoginSignup
9
4

More than 5 years have passed since last update.

goaでJWT認証

Posted at

goaでJWT認証を導入する

こちらで、goaを使ったマイクロサービス用のフレームワークのファーストステップについて記載しましたが、ここではJWTMiddlewareを使った、JWT認証の実現方法について記載します。
なお、こちらでは、middlewareを利用したgoaのログ出力について記載しております。

サンプルコードについて

本記事のサンプルコードはgithubで用意しております。

モデル(Repository)層を用意する

refresh tokenを使った再認証もしますので、モデル(Repository)層を用意しました。

テーブル定義

下記のようにrefresh_tokensテーブルを定義しました。

CREATE TABLE IF NOT EXISTS refresh_tokens
(
    jti VARCHAR(36) NOT NULL,
    user_id int unsigned UNIQUE NOT NULL,
    created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
    deleted_at datetime,
    PRIMARY KEY (jti)

) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

モデル(Repository)層の定義

サービス層から呼び出すモデル(Repository)で簡単なCRUDを定義しております。

認証のResourceを用意する

こちらのソースコードの説明になりますが、emailpasswordで認証をするログインAPIと、refresh tokenで再認証をするAPIの2つ用意します。

var JWT = JWTSecurity("jwt", func() {
    Header("Authorization")
    Scope("api:access", "API access") // Define "api:access" scope
})

var _ = Resource("auth", func() {
    Action("login", func() {
        Routing(POST("/login"))
        Payload(func() {
            Attribute("email", String, "name of sample", func() {
                Example("sample@goa-sample.test.com")
            })
            Attribute("password", String, "detail of sample", func() {
                Example("test1234")
            })
            Required("email", "password")
        })
        Response(OK, AuthSamples)
        Response(NotFound)
        Response(BadRequest, ErrorMedia)
    })
    Action("reauthenticate", func() {
        Routing(POST("/refresh_token"))
        Payload(func() {
            Attribute("refresh_token", String, "refresh token", func() {
                Example("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c")
            })
            Required("refresh_token")
        })
        Response(OK, AuthSamples)
        Response(NotFound)
        Response(BadRequest, ErrorMedia)
    })
})

また、Authorization:Bearerヘッダで認証をしますが、対象とするAPIについて下記のように記述します。

var _ = Resource("samples", func() {
    BasePath("/samples")
    Description("sample APIs with JWT Authorization")

    Security(JWT, func() {
        Scope("api:access")
    })

    Action("list", func() {
        Description("複数")
        Routing(
            GET("/"),
        )
        Response(OK, CollectionOf(MediaSamples))
        Response(NotFound)
        Response(BadRequest, ErrorMedia)
        Response(Unauthorized, ErrorMedia)
    })

middlewareを用意する

こちらのようにmiddlewareを用意します。
下記のように複数のRASの公開鍵をロードすることを前提にしております(サンプルで利用しているRSAの秘密鍵は1つですが・・・)。

func LoadJWTPublicKeys() ([]*rsa.PublicKey, error) {
    keyFiles, err := filepath.Glob("./jwtkey/*.pub")
    if err != nil {
        return nil, err
    }
    keys := make([]*rsa.PublicKey, len(keyFiles))
    for i, keyFile := range keyFiles {
        pem, err := ioutil.ReadFile(keyFile)
        if err != nil {
            return nil, err
        }
        key, err := jwtgo.ParseRSAPublicKeyFromPEM([]byte(pem))
        if err != nil {
            return nil, fmt.Errorf("failed to load key %s: %s", keyFile, err)
        }
        keys[i] = key
    }
    if len(keys) == 0 {
        return nil, fmt.Errorf("couldn't load public keys for JWT security")
    }

    return keys, nil
}

サービス層を用意する

認証用のサービス

認証に利用するサービス層はこちらで定義しました。
Authorization:Bearerヘッダで受け取ったtoken文字列を下記のようパースしています。

func (c *AuthSharedService) VerifyToken(tokenString string) (string, *sample_error.SampleError) {
    keys, err := sample_middleware.LoadJWTPublicKeys()
    if err != nil {
        return "", sample_error.NewSampleError(sample_error.InternalError, err.Error())
    }
    var parsedToken *jwt.Token
    for _, key := range keys {
        parsedToken, err = jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            // check signing method
            if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
                return nil, errors.New("Unexpected signing method")
            }
            return key, nil
        })
        if err != nil {
            break
        }
    }
    if err != nil {
        return "", sample_error.NewSampleError(sample_error.UnAuthorized, err.Error())
    }

    err = parsedToken.Claims.Valid()
    if err !=nil {
        return "", sample_error.NewSampleError(sample_error.UnAuthorized, err.Error())
    }

    claims := parsedToken.Claims.(jwt.MapClaims)

    return claims["jti"].(string), nil
}

Userサービス

Userサービスはこちらで用意しておりますが、transactionを使っております。
なお、パスワードはbcryptで暗号化しております。

func (s *UserService) AuthWithEmailAndPassword(email, password string) (*app.Auth, *sample_error.SampleError) {
    var Auth *app.Auth
    txFunc := func(db *gorm.DB) *sample_error.SampleError {
        h, err := NewHashedRefreshTokenService(db)
        if err != nil {
            return err
        }

        u, err := s.model.GetWithEmail(email, db)
        if err != nil {
            return err
        }

        err = Confirm(u.HashedPassword, password)
        if err != nil {
            return err
        }

        var jti string
        Auth, jti, err = s.Auth.IssueTokens(u.ID)
        if err != nil {
            return err
        }

        err = h.AddOrUpdate(u.ID, jti)
        return err
    }

    err := models.GormTransaction(s.model.Db, txFunc)
    if err != nil {
        return nil, err
    }

    return Auth, nil
}

transaction用のwrapperを用意しております。

func GormTransaction(db *gorm.DB, txFunc func(*gorm.DB) *sample_error.SampleError) (err *sample_error.SampleError) {
    tx := db.Begin()
    defer func() {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()

    err = txFunc(tx)
    return err
}

middlewareを指定する

認証用のcontrollerを用意し、main.goJWTMiddlewareを指定します。

    jwtMiddleware, err := sample_middleware.NewJWTMiddleware()
    if err != nil {
        fmt.Println(err)
        return;
    }
    app.UseJWTMiddleware(service, jwtMiddleware)

デモ

tokenrefresh tokenを取得する

スクリーンショット 2019-04-07 22.59.35.png
emailpasswordで認証するログインAPIを実行し、tokenrefresh tokenを取得します。

Authorization:Bearerヘッダで認証する

スクリーンショット 2019-04-07 23.12.14.png

Authorization:Bearerヘッダにtokenを指定して、APIを実行した結果です。

スクリーンショット 2019-04-07 23.00.30.png

tokenを指定しないとこのように401エラーとなります。

refresh tokenを使って再認証する

スクリーンショット 2019-04-07 23.14.09.png

このようにrefresh tokenを使って再認証します。

その他

各層でgo testをしたいので、せめてモデル(repository)層のdi層は用意したいと思います。
gomockassertrequireを使ったTable Driven Testについて次回は記載する予定です。

9
4
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
9
4