LoginSignup
65
57

More than 5 years have passed since last update.

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

Posted at

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.

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

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

65
57
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
65
57