AllIsHackedOff

Just a memo, just a progress

Firebase Authから返ってくるJWTをゴニョゴニョする (2)

前回の記事 allishackedoff.hatenablog.com の続きです。引き続きゴニョゴニョします。

JWTをDecodeする

golangのJWTのライブラリは github.com が最も使われているようです。 otiai10.hatenablog.com christina04.hatenablog.com この辺りでも利用されているようで、今回トライする際に参考にさせて頂きました。

Firebase Authから返ってくるJWTを検証する

検証するにはヘッダーをdecodeしないといけません。 headerに含まれる kid に対応するCERTIFICATEを使ってtokenが正しいかどうかを検証します。 「文字列としてcertificateを渡せばよしなにやってくれるやろ」とか甘いことを考えていましたが、しっかりrsa.PublicKey型のkeyを抜き出してやらないと verifyできません。

検証の流れは下記です。

  • JWTのヘッダーをDecode
  • "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com からcertificateのリストを取得
  • 取得したcertificateのリストから、 ヘッダーの kid に対応するkeyを見つける
    • 見つからない場合はexpireしている
  • 見つけたkeyをx509 パッケージを利用して rsa.PublicKey 型の変数にする
  • Tokenをdecode
  • decodeで生成されたjwt.Token Objectの Validを見る
    • expireしている / publickeyがうまく取得できていない場合は token.Valid == false になる

ソースコード (in Golang)

package main

import (
    "encoding/json"
    "encoding/pem"
    "errors"
    "fmt"
    "log"
    "net/http"
    "strings"

    "crypto/x509"

    "crypto/rsa"

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

type header struct {
    Alg string `json:"alg"`
    Kid string `json:"kid"`
}

type payload struct {
    jwt.StandardClaims
}

func main() {
    const (
        publicKeySrcURL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
    )
    rawToken := "<PASTE YOUR Firebase Auth JWT"
    segs := strings.Split(rawToken, ".")

    bHeader, err := jwt.DecodeSegment(segs[0])
    if err != nil {
        log.Fatalln("error in decoding segment header")
    }

    var h header
    json.Unmarshal(bHeader, &h)

    resp, err := http.Get(publicKeySrcURL)
    if err != nil {
        log.Fatalln("error in getting public key")
    }
    defer resp.Body.Close()
        
       // 一応Streamで扱っておく
    decoder := json.NewDecoder(resp.Body)
    pubkeyMap := make(map[string]string)
    err = decoder.Decode(&pubkeyMap)
    if err != nil {
        fmt.Printf("%v", err)
        log.Fatalln("error on decoding http response body")
    }

    rawPubkey, ok := pubkeyMap[h.Kid]
    if !ok {
        log.Fatalln("public key corresponding to XXX is not found")
    }
    pk, err := parseKey([]byte(rawPubkey))
    if err != nil {
        panic(err)
    }

    token, err := jwt.Parse(rawToken, func(token *jwt.Token) (interface{}, error) {
        if _, ok = token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, errors.New("signing methods other than RSA are not expected")
        }
        if token.Method != jwt.SigningMethodRS256 {
            return nil, errors.New("Signing methods other than RS256 are not expected")
        }

        return pk, nil
    })

    //spew.Dump(token)
    if !token.Valid {
        fmt.Println("token is invalid")
    }
}

// --CETIFICATE -> rsa.PublicKeyへの変換
func parseKey(rawPubkey []byte) (interface{}, error) {
    block, _ := pem.Decode(rawPubkey)
    cert, _ := x509.ParseCertificate(block.Bytes)
    pk := cert.PublicKey.(*rsa.PublicKey)

    fmt.Println(pk)
    return pk, nil
}

string -> []byteへのキャストをやっているのがイケてないですが、とりあえずこれでdecodeできます。

NOTE

Certificate -> rsa.PublicKeyへの変換が所見だったのでこのあたりの知識を補強しておく