GAE/Go で公開鍵暗号を使う JWT 認証サーバーを作った

# リポジトリ

* https://github.com/nirasan/gae-jwt/tree/qiita-daa654f4f0e2321aedaa

# 特徴

* Google App Engine 上で動作する
* 名前とパスワードでユーザー登録ができる
* 名前とパスワードでユーザー認証を行い JWT トークンを発行する
* JWT トークンで認可されたユーザーにコンテンツを提供できる

# GAE/Go 特有の実装ポイント

## ローカルファイルを持てない

* GAE ではローカルのファイルシステムを利用できない
* ファイルシステムとしては Cloud Storage などがある
* 今回は公開鍵ファイルと秘密鍵ファイルは `github.com/jteeuwen/go-bindata` を使って go のファイルに変換して利用した

## デプロイ用のディレクトリに実行ファイルと設定ファイルを用意する

* デプロイ用のディレクトリはプロジェクトルート直下の `app`
* `app/app.go` が実行ファイルで `init` が GAE から実行される
* `app/app.yaml` は最低限の設定ファイル
  * 設定ファイルの概要は https://cloud.google.com/appengine/docs/python/configuration-files

## データベースには Datastore を利用する

* Google Cloud Platform ではデータベースとして NoSQL の Datastore や SQL が使える Cloud SQL などが利用できる
* 今回は無料枠があり簡単なクエリくらいなら実行できる Datastore を使う

# 簡単な解説

## GAE 用のファイル構成

* GOPATH の切り替えなどは行わず通常の GOPATH 以下にプロジェクトを作成する
* app 以下に GAE 用のファイルを配置し、自プロジェクトのフルパスでファイルをインポートしてハンドラーを登録する

```go:app/app.go

import (
	"github.com/nirasan/gae-jwt/handler"
	"net/http"
)

func init() {
	http.Handle("/", handler.NewHandler())
}
```

## ユーザーの登録処理

* handler.RegistrationHandler で実装
* Datastore へユーザー情報を登録する
* パスワードは bcrypt でハッシュ化して保存する

```go:handler.go
	// ユーザー情報の登録準備
	ctx := appengine.NewContext(r)
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		panic(err.Error())
	}
	ua := UserAuthentication{Username: req.Username, Password: string(hashedPassword)}

	// Datastore へユーザー情報を登録
	err = datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		key := datastore.NewKey(ctx, "UserAuthentication", req.Username, 0, nil)
		var userAuthentication UserAuthentication
		if err := datastore.Get(ctx, key, &userAuthentication); err != datastore.ErrNoSuchEntity {
			return errors.New("user already exist")
		}
		if _, err := datastore.Put(ctx, key, &ua); err == nil {
			return nil
		} else {
			return err
		}
	}, nil)
```

## ユーザーの認証処理

* handler.AuthenticationHandler で実装

### ユーザーの参照

* Datastore を検索してユーザーが存在するか確認する
* パスワードは bcrypt で検証する

```go:handler.go
	// ユーザーが存在するかどうか確認
	ctx := appengine.NewContext(r)
	key := datastore.NewKey(ctx, "UserAuthentication", req.Username, 0, nil)
	var userAuthentication UserAuthentication
	if err := datastore.Get(ctx, key, &userAuthentication); err != nil {
		EncodeJson(w, AuthenticationHandlerResponse{Success: false})
		return
	}
	// パスワードの検証
	if err := bcrypt.CompareHashAndPassword([]byte(userAuthentication.Password), []byte(req.Password)); err != nil {
		EncodeJson(w, AuthenticationHandlerResponse{Success: false})
		return
	}
```
### 秘密鍵の読み込み

* go-bindata で固めたファイルから秘密鍵を読み込む

```go:handler.go
	pem, e := bindata.Asset("assets/ec256-key-pri.pem")
```

### JWT トークンの作成と署名

```go:handler.go
	// 署名アルゴリズムの作成
	method := jwt.GetSigningMethod("ES256")
	// トークンの作成
	token := jwt.NewWithClaims(method, jwt.MapClaims{
		"sub": req.Username,
		"exp": time.Now().Add(time.Hour * 1).Unix(),
	})
	// 秘密鍵のパース
	privateKey, e := jwt.ParseECPrivateKeyFromPEM(pem)
	if e != nil {
		panic(e.Error())
	}
	// トークンの署名
	signedToken, e := token.SignedString(privateKey)
	if e != nil {
		panic(e.Error())
	}
```

## 認可したユーザーのみ閲覧可能なコンテンツを提供

* handler.AuthorizedHelloWorldHandler で実装
* handler.Authorization でリクエストからトークンを取得して検証する

```go:handler.go
	token, e := Authorization(r)
  
	if e != nil {
		EncodeJson(w, HelloWorldHandlerResponse{Success: false})
	}
```

# インストール

* [App Engine SDK](https://cloud.google.com/appengine/downloads) をインストールする
* 以下のパッケージをインストールする

```
go get -u github.com/dgrijalva/jwt-go
go get -u github.com/gorilla/mux
go get -u google.golang.org/appengine/datastore
go get -u google.golang.org/appengine
go get -u google.golang.org/appengine/log
go get -u github.com/jteeuwen/go-bindata/...
```

# 開発環境を起動する

* プロジェクトのルートで以下のコマンドを実行する

```
goapp serve app
```

* デプロイ

* Google Cloud Platform でプロジェクトを作成
* 以下のコマンドをプロジェクトのルート実行してデプロイ

```
appcfg.py -A <PROJECT_ID> -V v1 update app/
```

# 公開鍵と秘密鍵を準備する

## openssl で鍵の作成

```
mkdir assets
cd assets
openssl ecparam -genkey -name prime256v1 -noout -out ec256-key-pair.pem
openssl ec -in ec256-key-pair.pem -outform PEM -pubout -out ec256-key-pub.pem
openssl ec -in ec256-key-pair.pem -outform PEM -out ec256-key-pri.pem
```

## go-bindata で鍵を go の実行ファイルに変換

```
go-bindata -o bindata/bindata.go assets
```

## 鍵の実行ファイルの編集

* パッケージ名を 'main' から 'bindata' に変更

# cURL でリクエストを実行する

## ユーザー登録

```
curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"username":"USERNAME","password":"PASSWORD"}' http://<PROJECT_ID>.appspot.com/registration
```

## ユーザー認証

```
curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"username":"USERNAME","password":"PASSWORD"}' http://<PROJECT_ID>.appspot.com/authentication
```

## 誰でもアクセスできるコンテンツの表示

```
curl -H "Accept: application/json" -H "Content-type: application/json" http://<PROJECT_ID>.appspot.com/hello
```

## 認可されたユーザーだけアクセスできるコンテンツの表示

* `<TOKEN>` にはユーザー認証のレスポンスの `Token` 要素の値を挿入する

```
curl -H "Accept: application/json" -H "Content-type: application/json" -H "Authorization: Bearer <TOKEN>" http://<PROJECT_ID>.appspot.com/authorized_hello
```