Go
golang
JWT
jwt-go

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

GolangでJWT(JSON Web Token)を取り扱いたいと思い、jwt-goの使い方を調べました。

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

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


Routing定義

main.goに、ルーティングの定義をします。この記事では、julienschmidt/httprouterを使用しています。


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、変数宣言


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フォルダに移動されました。

そのため、

jwt "github.com/dgrijalva/jwt-go"

request "github.com/dgrijalva/jwt-go/request"

の様に別々importしています。

今回、RSA256を利用し、JWT発行時は秘密鍵でサインを行い、JWT認証時は公開鍵で認証を行います。

そのため、verifyKey, signKeyを宣言しました。

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

$ 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に変更します。

こちらの記載にある通り、公開鍵を読み込むにはpksc1もしくはpksc8でないといけない為です。

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


LoginHandler(JWTの発行)


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に変更になった際に仕様が変更になりました。こちらを参照

version2のときは、


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の認証)


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だと思います。


version3

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


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

こちらの通り、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でした。


version2

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


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

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

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


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())
}

}


実行すると、

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を使って、認証が通るか試してみます。


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())
}
}


結果は、

code is 200

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

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

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