Firebase Authentication生成JWTをGoサーバでバリデーションする


はじめに

現在(2018年11月12日時点)のFirebase Authenticationが提供する認証用のトークンはJWTです。

公式ドキュメントにもあるように、Firebaseでは各種言語のSDKが用意されており、Go言語用のSDKも提供されています。

本記事では上記SDKを利用しない、Goの代表的なJWT用サードパーティのjwt-goを利用したバリデーション実装方法を示します。

そもそもJWTとは何なのか、どう使うのかに関しては公式ドキュメントや他記事をご参照ください。

環境

  • Go: go version 1.11
  • jwt-go: v3.2.0
  • MacOS: High Sierra 10.13.6

Firebase AuthenticationのJWT用公開鍵利用について

JWTのバリデーションをするには発行元が提供する公開鍵が必要です。

こちらに記載されているように、Firebase発行の公開鍵はこのリンク上にJSON形式で複数あり、検証側は始めに利用するべき公開鍵を特定するためのkid(Key ID)をJWTから取得する必要があります。

実装の流れとしては以下のようになります。

  1. リクエストにあるJWTをJSONとしてパースしkidの値を得る。
  2. Firebaseの公開鍵一覧から取得したkidに対応する公開鍵を特定する。
  3. 公開鍵を利用してJWTを検証する。

実際のコード

動くHTTPサーバを作りました。

ファイル構成は以下になります。

.
├── auth.go             <= JWTバリデーションをする。
├── helpers.go          <= googleapis_key.jsonから公開鍵リストを取得する。
├── main.go             <= HTTPサーバを立てる。
└── publicKeys
    └── googleapis_keys.json
main.go
package main

import (
    "log"
    "net/http"

    "github.com/gorilla/pat"
)

type regularHandler struct{}

func (h *regularHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("200 - OK!"))
}

func main() {
    // 公開鍵JSONからkid対応したmapを取得。
    publicKeys := LoadRSAPublicKeyFromJsonFile("./publicKeys/googleapis_keys.json")

    // Route setting
    p := pat.New()
    p.Add("GET", "/", MustAuth(&regularHandler{}, publicKeys))

    // Launch web server
    log.Fatal(http.ListenAndServe(":5000", p))
}
auth.go
package main

import (
    "net/http"
    "crypto/rsa"
    "log"
    "fmt"

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

func MustAuth(handler http.Handler, publicKeys map[string]*rsa.PublicKey) http.Handler {
    return &authHandler{next: handler, publicKeys: publicKeys}
}

type authHandler struct {
    next http.Handler
    publicKeys map[string]*rsa.PublicKey
}

func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // サンプルのトークンを利用する。【注意】↓のJWTのトークンはダミーなので実際にFirebaseから発行されたJWTを利用ください。
    tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
    // Authorizationヘッダーにトークンが付与されている想定。
    r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", tokenString))

    // JWTをパースしてFirebase定義のkidの値を得る。この時点ではJWTとしての検証は行わない(行えない)。
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return []byte("DummyBytes"), nil
    })
    if err.Error() != "key is of invalid type" {
        log.Println("Can't parse tokenString correctly. Error message:", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    kid, ok := token.Header["kid"].(string)
    if !ok {
        log.Println("Can't get token.Header['kid']")
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    // 取得したkidに対応する公開鍵を利用してJWTを検証する。
    token, err = request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(*jwt.Token) (interface{}, error) {
        return h.publicKeys[kid], nil
    })
    if err != nil {
        log.Println("Can't Parse from request. Error message:", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    if !token.Valid {
        log.Println("Request token is invalid. token.Header:", token.Header, "token.Claims", token.Claims)
        w.WriteHeader(http.StatusUnauthorized)
        return
    }

    log.Println("Got an valid token. token.Header:", token.Header, "token.Claims", token.Claims)
    // 検証OKならば本来のHTTPハンドリングへ。
    h.next.ServeHTTP(w, r)
}
helpers.go
package main

import (
    "crypto/rsa"
    "io/ioutil"
    "encoding/json"

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

func LoadRSAPublicKeyFromJsonFile(location string) map[string]*rsa.PublicKey {
    jsonData, err := ioutil.ReadFile(location)
    if err != nil {
        panic(err.Error())
    }
    var objMap map[string]string
    err = json.Unmarshal(jsonData, &objMap)
    if err != nil {
        panic(err.Error())
    }
    result := make(map[string]*rsa.PublicKey)
    for k, v := range objMap {
        key, err := jwt.ParseRSAPublicKeyFromPEM([]byte(v))
        if err != nil {
            panic(err.Error())
        }
        result[k] = key
    }
    return result
}

参考