goaでJWT認証

# goaでJWT認証を導入する
[こちら](https://qiita.com/hiroykam/items/398875ef4cff4d35454e)で、`goa`を使ったマイクロサービス用のフレームワークのファーストステップについて記載しましたが、ここでは`JWTMiddleware`を使った、`JWT`認証の実現方法について記載します。
なお、[こちら](https://qiita.com/hiroykam/items/3bf71b3d16c3e08c44ad)では、`middleware`を利用した`goa`のログ出力について記載しております。

## サンプルコードについて
本記事のサンプルコードは[github](https://github.com/hiroykam/goa-sample)で用意しております。

## モデル（Repository）層を用意する
`refresh token`を使った再認証もしますので、モデル（Repository）層を用意しました。

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

```sql
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)](https://github.com/hiroykam/goa-sample/blob/master/models/refresh_token.go)で簡単な`CRUD`を定義しております。

## 認証の`Resource`を用意する
[こちらのソースコード](https://github.com/hiroykam/goa-sample/blob/master/design/resources.go)の説明になりますが、`email`と`password`で認証をするログインAPIと、`refresh token`で再認証をするAPIの2つ用意します。


```go
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について下記のように記述します。

```go
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`を用意する
[こちら](https://github.com/hiroykam/goa-sample/blob/master/sample_middleware/jwt.go)のように`middleware`を用意します。
下記のように複数のRASの公開鍵をロードすることを前提にしております（サンプルで利用しているRSAの秘密鍵は1つですが・・・）。

```go
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
}
```

## サービス層を用意する
### 認証用のサービス
認証に利用するサービス層は[こちら](https://github.com/hiroykam/goa-sample/blob/master/services/auth.go)で定義しました。
`Authorization:Bearerヘッダ`で受け取った`token`文字列を下記のようパースしています。

```go
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`サービスは[こちら](https://github.com/hiroykam/goa-sample/blob/master/services/user.go)で用意しておりますが、transactionを使っております。
なお、パスワードは`bcrypt`で暗号化しております。

```go
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を用意しております。

```go
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](https://github.com/hiroykam/goa-sample/blob/master/controller/auth.go)を用意し、`main.go`で`JWTMiddleware`を指定します。

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


## デモ
### `token`と`refresh token`を取得する
<img width="862" alt="スクリーンショット 2019-04-07 22.59.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135559/b90d30a1-5a9c-d1ad-9f5c-5972e7cb212a.png">
`email`と`password`で認証するログインAPIを実行し、`token`と`refresh token`を取得します。

### `Authorization:Bearerヘッダ`で認証する
<img width="899" alt="スクリーンショット 2019-04-07 23.12.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135559/d2b480c4-0b62-8d61-ee97-a47c37a3afd3.png">

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

<img width="820" alt="スクリーンショット 2019-04-07 23.00.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135559/9f112e5d-a94b-9d83-295e-c9ce4085c658.png">

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


### `refresh token`を使って再認証する
<img width="1001" alt="スクリーンショット 2019-04-07 23.14.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135559/a284fc68-f437-2068-9055-d829bc5a19e0.png">

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


##その他
各層で`go test`をしたいので、せめてモデル（repository）層の`di`層は用意したいと思います。
`gomock`、`assert`、`require`を使った`Table Driven Test`について次回は記載する予定です。