jwt-goを用いた、GolangでのJWTの発行、認証について

GolangでJWT（JSON Web Token）を取り扱いたいと思い、[jwt-go](https://github.com/dgrijalva/jwt-go)の使い方を調べました。

JWTについての説明は他の有益な解説サイトがあるので、そちらを参考にして下さい。

早速実装の説明に入ります。


# Routing定義
main.goに、ルーティングの定義をします。この記事では、[julienschmidt/httprouter](https://github.com/julienschmidt/httprouter)を使用しています。


```golang:main.go
package main

import (
	"fmt"
	"html/template"
	"log"
	"net/http"
	"sync"

	"github.com/julienschmidt/httprouter"
)

type templateHandler struct {
	once     sync.Once
	filename string
	templ    *template.Template
}

type API struct {
	router *httprouter.Router
}

func main() {
	fmt.Println("running...")

	api := API{
		router: httprouter.New(),
	}

	api.setAppRouter()

	log.Fatal(http.ListenAndServe(":8080", api.router))
}

func (api *API) setAppRouter() {
	api.router.POST("/tokenAuth", LoginHandler)                             //Login
	api.router.GET("/tokenAuthenticate", RequireTokenAuthenticationHandler) //Tokenが有効か確認
}

```

tokenAuth でJWTの発行、tokenAuthenticateで発行されたJWTが有効か確認します。

# Handlerの定義

説明をしやすくするため、handler.goの中身を三分割して説明を行います。

## import、変数宣言
```golang:handler.go(import、変数宣言)
package main

import (
	"crypto/rsa"
	"fmt"
	"io/ioutil"
	"net/http"
	"time"

	jwt "github.com/dgrijalva/jwt-go"
	request "github.com/dgrijalva/jwt-go/request"
	"github.com/julienschmidt/httprouter"
)

var (
	verifyKey *rsa.PublicKey
	signKey   *rsa.PrivateKey
)
```

jwt-goは、[version2からversion3に変わった際、JWTを読み込むParseFromRequestメソッドが、requestフォルダに移動されました。](https://github.com/dgrijalva/jwt-go/blob/master/VERSION_HISTORY.md)
そのため、

```
jwt "github.com/dgrijalva/jwt-go"
request "github.com/dgrijalva/jwt-go/request"
```
の様に別々importしています。

今回、RSA256を利用し、JWT発行時は秘密鍵でサインを行い、JWT認証時は公開鍵で認証を行います。
そのため、verifyKey, signKeyを宣言しました。

プロジェクトパス以下に秘密鍵、公開鍵を作成します。

```terminal
$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key:
${projectのパス}/demo.rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ${projectのパス}/demo.rsa.
Your public key has been saved in ${projectのパス}/demo.rsa.pub.
The key fingerprint is:
SHA256:8nz3BBJQKWrivQ8Msa/UT92R/JgQTKqKeLWrf7kVXHg 
@Mac.local
The key's randomart image is:
+---[RSA 2048]----+
|        ..o.     |
|        .*.      |
|    .  .o.E      |
|    .ooo o + .   |
|   .++o S o =    |
| . o.B.+ o + *   |
|. o + =o= o = o  |
| . . o+= . . o   |
|  .o+..oo     .  |
+----[SHA256]-----+
```


公開鍵のフォーマットをpksc8に変更します。
[こちら](https://godoc.org/github.com/dgrijalva/jwt-go#ParseRSAPublicKeyFromPEM)の記載にある通り、公開鍵を読み込むにはpksc1もしくはpksc8でないといけない為です。

```terminal
$ssh-keygen -f demo.rsa.pub -e -m pkcs8 > demo.rsa.pub.pkcs8
```

##LoginHandler（JWTの発行）

```golang:handler.go(LoginHandler)
// LoginHandler : JWTの発行
func LoginHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {

//今回、username,password共にtestを使っていますが、皆様が実際に行う場合はbodyから読み込んだり、DBから読み込んだりして下さい。
	username := "test" 
	password := "test" 

	signBytes, err := ioutil.ReadFile("./demo.rsa")
	if err != nil {
		panic(err)
	}

	signKey, err := jwt.ParseRSAPrivateKeyFromPEM(signBytes)
	if err != nil {
		panic(err)
	}

	if username == "test" && password == "test" {
		// create token
		token := jwt.New(jwt.SigningMethodRS256)

		// set claims
		claims := token.Claims.(jwt.MapClaims)
		claims["name"] = "test"
		claims["admin"] = true
		claims["exp"] = time.Now().Add(time.Hour * 72).Unix()

		tokenString, err := token.SignedString(signKey)
		if err != nil {
			fmt.Println(err)
		}
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(tokenString))
	}
}
```

秘密鍵は、読み込んで[]byte型になったsignBytesをParseRSAPrivateKeyFromPEMで*PrivateKey型のsignKeyに格納しています。


claimsの箇所もversion2からvarsion3に変更になった際に仕様が変更になりました。[こちらを参照](https://github.com/dgrijalva/jwt-go/blob/master/VERSION_HISTORY.md#300)

version2のときは、

```golang:version2
token := jwt.New(jwt.SigningMethodRS256)
token.Claims["name"] = "test"
token.Claims["admin"] = true
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
```

と書いていました。


##RequireTokenAuthenticationHandler（JWTの認証）

```golang:handler.go(RequireTokenAuthenticationHandler)
// RequireTokenAuthenticationHandler : Tokenの検証
func RequireTokenAuthenticationHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {

	verifyBytes, err := ioutil.ReadFile("./demo.rsa.pub.pkcs8")
	if err != nil {
		panic(err)
	}
	verifyKey, err := jwt.ParseRSAPublicKeyFromPEM(verifyBytes)
	if err != nil {
		panic(err)
	}

	token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) {
		_, err := token.Method.(*jwt.SigningMethodRSA)
		if !err {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		} else {
			return verifyKey, nil
		}
	})
	if err == nil && token.Valid {
		w.WriteHeader(http.StatusOK)
	} else {
		w.WriteHeader(http.StatusUnauthorized)
	}
}
```

公開鍵については、秘密鍵と似た方法で、読み込んで[]byte型になったverifyBytesをParseRSAPublicKeyFromPEMで*PublicKey型のverifyKeyに格納しています。

認証時にややこしい箇所は、下記のParseFromRequestだと思います。

```golang:version3
token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) { ...
```

version2からversion3になった際、ParseFromRequestメソッドに渡す引数が増えました。それが、今回でいうと、request.AuthorizationHeaderExtractor　です。

[こちらの通り](https://github.com/dgrijalva/jwt-go/blob/master/VERSION_HISTORY.md#300)、http.request内のどこにJWTがあるのかを指定することになりました。

> Added new interface type Extractor, which is used for extracting JWT strings from http requests. Used with ParseFromRequest and ParseFromRequestWithClaims.

version2の際は、下記の様な書き方でOKでした。

```golang:version2
token, err := ParseFromRequest(r, func(token *jwt.Token) (interface{}, error) { ...
```

httpリクエストでJWTを送る場合、リクエストヘッダのAuthorizationを用いることが多いと思うので、AuthorizationHeaderExtractorを今回使用しています。


それでは早速動作確認を行ってみます。

下記の様なテストを書きました。

```golang:handler_test.go(TestLoginHandler)
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/julienschmidt/httprouter"
)

func TestLoginHandler(t *testing.T) {
	router := httprouter.New()
	router.POST("/tokenAuth", LoginHandler)

	req, err := http.NewRequest("POST", "/tokenAuth", nil)
	if err != nil {
		t.Error("NewRequest URI error")
	}
	w := httptest.NewRecorder()

	router.ServeHTTP(w, req)
	if w.Code != 200 {
		fmt.Printf("api error. code is %d\n", w.Code)
		fmt.Printf("api error. header is %#v\n", w.Header())
		fmt.Printf("api error. body is %#v\n", w.Body.String())
	} else {
		fmt.Printf("code is %d\n", w.Code)
		fmt.Printf("header is %#v\n", w.Header())
		fmt.Printf("body is %#v\n", w.Body.String())
	}

}
```

実行すると、

```terminal
code is 200
header is http.Header{}
body is "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTI3OTY1ODU3LCJuYW1lIjoidGVzdCJ9.a_wC2avD1HhDWypx-xGZo0Vo3Jwe7gpezNGhiSFOm_zR_h1ACcqzFLbYhl1YDgJ2ijTcZD0GUGknZpBOGpYR_fQKQf8i-TnzmYicwzS8MW8p8_X4MOdIyg2_AHsTGsMr5-vW5d3Qd64HHsn45Dz_-TQRhqr1Ws0R2T1GlnEo9bR3ylPBuI9HhYtosSD8mOTiYZYLqbZnl5rRVu0mGtNNHNuEsvxebCLf0o1xdBN-zUzZdaofiLQYtDL9NTvG0taosM5e74HcDfNK2xvnbEXO-MoXttzMVfZK55H2rLdcl9FchUdAfji7-zfXALwp7CCZVMYqq0tlt9Ltw8H8cfzGNw"
PASS
ok  	{プロジェクトパス}	0.018s
Success: Tests passed.
```

と、responsebodyの中にJWTが返ってきました。


このJWTを使って、認証が通るか試してみます。

```golang:handler_test.go(TestRequireTokenAuthentication)
func TestRequireTokenAuthentication(t *testing.T) {
	router := httprouter.New()
	router.GET("/tokenAuthenticate", RequireTokenAuthenticationHandler)

	req, err := http.NewRequest("GET", "/tokenAuthenticate", nil)
	if err != nil {
		t.Error("NewRequest URI error")
	}
	w := httptest.NewRecorder()

	req.Header.Set("Authorization", "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTI3OTY1ODU3LCJuYW1lIjoidGVzdCJ9.a_wC2avD1HhDWypx-xGZo0Vo3Jwe7gpezNGhiSFOm_zR_h1ACcqzFLbYhl1YDgJ2ijTcZD0GUGknZpBOGpYR_fQKQf8i-TnzmYicwzS8MW8p8_X4MOdIyg2_AHsTGsMr5-vW5d3Qd64HHsn45Dz_-TQRhqr1Ws0R2T1GlnEo9bR3ylPBuI9HhYtosSD8mOTiYZYLqbZnl5rRVu0mGtNNHNuEsvxebCLf0o1xdBN-zUzZdaofiLQYtDL9NTvG0taosM5e74HcDfNK2xvnbEXO-MoXttzMVfZK55H2rLdcl9FchUdAfji7-zfXALwp7CCZVMYqq0tlt9Ltw8H8cfzGNw")

	router.ServeHTTP(w, req)
	if w.Code != 200 {
		fmt.Printf("api error. code is %d\n", w.Code)
		fmt.Printf("api error. header is %#v\n", w.Header())
		fmt.Printf("api error. body is %#v\n", w.Body.String())
	} else {
		fmt.Printf("code is %d\n", w.Code)
		fmt.Printf("header is %#v\n", w.Header())
		fmt.Printf("body is %#v\n", w.Body.String())
	}
}

```

結果は、

```terminal
code is 200
header is http.Header{}
body is ""
PASS
ok  	{プロジェクトパス}	0.017s
Success: Tests passed.
```


と、無事認証が通ったことが確認できました。

（わかりにくい説明箇所もあったと思うので、随時更新予定です。）