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


リポジトリ

特徴

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

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

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

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

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

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

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

簡単な解説

GAE 用のファイル構成

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

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

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

ユーザーの登録処理

  • handler.RegistrationHandler で実装
  • Datastore へユーザー情報を登録する
  • パスワードは bcrypt でハッシュ化して保存する
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 で検証する
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 で固めたファイルから秘密鍵を読み込む
handler.go
    pem, e := bindata.Asset("assets/ec256-key-pri.pem")

JWT トークンの作成と署名

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 でリクエストからトークンを取得して検証する
handler.go
    token, e := Authorization(r)

    if e != nil {
        EncodeJson(w, HelloWorldHandlerResponse{Success: false})
    }

インストール

  • App Engine SDK をインストールする
  • 以下のパッケージをインストールする
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