🔑

FireBase Authにより取得したJWTのGolangによる検証

2023/07/07に公開

※このコードで実際に動くことは手動で検証していますが、記事はChatGPTを用いて書いています

Firebase Authentication はGoogleが提供する認証システムで、EmailやPasswordだけでなく、ソーシャルログインなども簡単に導入することができます。Firebase AuthenticationはJWT (Json Web Token) を使用して認証情報をクライアントに渡すため、サーバーサイドでそのJWTを検証することでユーザーの認証状態を確認することができます。

この記事ではGo言語を使ってFirebaseから発行されたJWTの検証方法を示します。

まずはじめに、以下のようなカスタムクレームを定義します。

type CustomClaims struct {
	Name     string `json:"name"`
	Picture  string `json:"picture"`
	Iss      string `json:"iss"`
	Aud      string `json:"aud"`
	AuthTime int64  `json:"auth_time"`
	UserId   string `json:"user_id"`
	Sub      string `json:"sub"`
	Iat      int64  `json:"iat"`
	Exp      int64  `json:"exp"`
	Email    string `json:"email"`
	jwt.StandardClaims
}

これらはFirebaseから発行されるJWTが持つ情報の一部です。

そして以下のような関数checkFirebaseJWTを定義します。この関数では以下の手順でJWTの検証を行います。

  1. Firebaseの公開鍵を取得します。
  2. JWTのヘッダー部分を解析し、使用された鍵のID(kid)を取得します。
  3. 鍵のIDから対応する公開鍵を取得し、公開鍵の型をrsa.PublicKeyにキャストします。
  4. 公開鍵を使用してJWTの署名を検証します。
  5. トークンが有効な場合、そのクレームを返します。
func checkFirebaseJWT(tokenString string)(CustomClaims,error){
	// Googleの公開鍵を取得
	resp, err := http.Get("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")
	// 省略
	
	// JWTのヘッダを解析し署名に用いられている鍵を取得
	parts := strings.Split(tokenString, ".")
	// 省略
	
	// 鍵のIDから対応する公開鍵を取得し、公開鍵の型をrsa.PublicKeyにキャスト
	kid := header["kid"].(string)
	certString := result[kid].(string)
	// 省略
	rsaPublicKey := cert.PublicKey.(*rsa.PublicKey)

	// 署名を検証
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return rsaPublicKey, nil
	})
	// 省略
}

以下に全体の詳細な手順を示します。

Googleの公開鍵を取得

Firebaseによって発行されたJWTはGoogleの公開鍵を使って署名が行われています。そのため、まずGoogleの公開鍵を取得します。公開鍵は以下のURLから取得することができます。

resp, err := http.Get("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")

取得した公開鍵はX.509証明書としてフォーマットされているので、それを後で解析する必要があります。

JWTのヘッダを解析し署名に用いられている鍵を取得

次に、JWTを'.'で分割し、そのヘッダ部分をBase64デコードしてJSONをパースします。これにより、ヘッダ情報として保存されている公開鍵のID(kid)を取得します。

parts := strings.Split(tokenString, ".")
headerJson, err := base64.RawURLEncoding.DecodeString(parts[0])

鍵のIDから対応する公開鍵を取得し、公開鍵の型をrsa.PublicKeyにキャスト

公開鍵のIDから対応する公開鍵を取得し、PEM形式からrsa.PublicKeyへキャストします。これにより、公開鍵をGoのcrypto/rsaパッケージが理解できる形に変換します。

kid := header["kid"].(string)
certString := result[kid].(string)
block, _ := pem.Decode([]byte(certString))
cert, err := x509.ParseCertificate(block.Bytes)
rsaPublicKey := cert.PublicKey.(*rsa.PublicKey)

署名を検証

最後に、jwt-goパッケージのParseWithClaims関数を使って、JWTの署名を検証します。この関数には先ほど取得した公開鍵を渡します。

token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
	return rsaPublicKey, nil
})

以上の手順を経ることで、Firebaseから発行されたJWTの検証が可能となります。検証した結果、無効なトークンであった場合や有効期限が切れていた場合などにはエラーを返し、有効なトークンであった場合にはその中のクレーム情報を返します。

Firebase Authを用いた認証は多くのWebサービスやアプリで使用されていますが、その裏で行われているJWTの検証の流れを理解することで、より安全なシステムを構築# Firebase Authにより取得したJWTのGolangによる検証

Firebase Authenticationは、Googleが提供する認証サービスで、Google、FacebookなどのSNSアカウントだけでなく、メールアドレスとパスワードによる認証も簡単に実装することができます。認証が成功すると、サーバーからJWT(JSON Web Token)がクライアントに送信されます。

しかし、このJWTが本当にFirebaseから発行されたものであることをどのように確認すれば良いでしょうか?ここでは、Go言語を使用して、Firebaseから発行されたJWTの検証方法を紹介します。

JWTの概要

まず、JWTについての簡単な説明から始めましょう。JWTは、電子署名されたURL安全な文字列であり、クライアントとサーバー間で情報を安全に転送するための規格です。JWTは次の3部分から構成されています。

  1. ヘッダ(Header):署名や暗号化に使用するアルゴリズムなどのメタデータを保持しています。
  2. ペイロード(Payload):トークンに含めたい任意のデータ(クレーム)を保持しています。
  3. 署名(Signature):ヘッダとペイロードのハッシュ値を秘密鍵で暗号化したものです。
    これら3つの部分はそれぞれBase64Urlでエンコードされており、それぞれを.でつなげることで1つのJWTが生成されます。

JWTの検証

ここでは、Firebaseから発行されたJWTを検証するためのGo言語のコードについて説明します。

まずはじめに、CustomClaimsという構造体を定義します。これは、Firebaseから発行されたJWTのペイロード部分に含まれるクレームを表現しています。

type CustomClaims struct {
	Name     string `json:"name"`
	Picture  string `json:"picture"`
	Iss      string `json:"iss"`
	Aud      string `json:"aud"`
	AuthTime int64  `json:"auth_time"`
	UserId   string `json:"user_id"`
	Sub      string `json:"sub"`
	Iat      int64  `json:"iat"`
	Exp      int64  `json:"exp"`
	Email    string `json:"email"`
	jwt.StandardClaims
}

次に、checkFirebaseJWTという関数を定義します。この関数は、JWTを引数として受け取り、そのJWTがFirebaseから発行されたものであることを確認するために以下の処理を行います。

  1. Googleの公開鍵を取得します。
  2. JWTのヘッダから、公開鍵のID(kid)を取得します。
  3. 取得した公開鍵IDに対応する公開鍵を用いて、JWTの署名を検証します。
  4. 署名が正しい場合、JWTのペイロードからクレームを取得します。
    以下に、その具体的なコードを示します。
package main

import (
	"fmt"
	"log"
	"crypto/rsa"
	"crypto/x509"
	"github.com/dgrijalva/jwt-go"
	"encoding/pem"
	"time"
	"io/ioutil"
	"net/http"
	"encoding/json"
	"encoding/base64"
	"strings"
	"errors"
)

type CustomClaims struct {
	Name     string `json:"name"`
	Picture  string `json:"picture"`
	Iss      string `json:"iss"`
	Aud      string `json:"aud"`
	AuthTime int64  `json:"auth_time"`
	UserId   string `json:"user_id"`
	Sub      string `json:"sub"`
	Iat      int64  `json:"iat"`
	Exp      int64  `json:"exp"`
	Email    string `json:"email"`
	jwt.StandardClaims
}

func main() {
	// exsample
	tokenString := "" // Your JWT here
	CustomClaims,err := checkFirebaseJWT(tokenString)
	fmt.Println(CustomClaims,err)
}

// JWTを検証する関数
func checkFirebaseJWT(tokenString string)(CustomClaims,error){

	// Googleの公開鍵を取得
	resp, err := http.Get("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")
	if err != nil {
		log.Fatalf("Failed to make a request: %v", err)
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("Failed to read the response body: %v", err)
	}

	var result map[string]interface{}
	err = json.Unmarshal([]byte(body), &result)

	if err != nil {
		log.Fatalf("Failed to json unmarshal: %v", err)
	}

	// JWTのヘッダを解析し署名に用いられている鍵を取得
	parts := strings.Split(tokenString, ".")

	// decode the header
	headerJson, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		log.Fatalf("Error decoding JWT header:", err)
		return CustomClaims{},err
	}

	var header map[string]interface{}
	err = json.Unmarshal(headerJson, &header)
	if err != nil {
		log.Fatalf("Error unmarshalling JWT header:", err)
		return CustomClaims{},err
	}

	kid := header["kid"].(string)
	certString := result[kid].(string)
	block, _ := pem.Decode([]byte(certString))
	if block == nil {
		log.Fatalf("failed to parse PEM block containing the public key")
		return CustomClaims{},err
	}

	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		log.Fatalf("failed to parse certificate: ", err)
		return CustomClaims{},err
	}

	rsaPublicKey := cert.PublicKey.(*rsa.PublicKey)

	// 署名を検証
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return rsaPublicKey, nil
	})

	if err != nil {
		log.Fatalf("Error while parsing token: %v\n", err)
		return CustomClaims{},err
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		if time.Unix(claims.Exp, 0).Before(time.Now()) {
			return CustomClaims{},errors.New("Token is valid. But token is expired.")
		} else {
			return *claims, nil
		}
	} else {
		return CustomClaims{},errors.New("Token is not valid")
	}
}

これにより、JWTの署名が正しいかどうかを検証し、正しい場合はそのクレームを取得することができます。

Discussion