Golang GinでAuth0のJWTを検証する

公開日: 更新日:

こんにちは。株式会社フルーデンスの小巻です。

前回の記事にも書いたので重複しますが、現在、Angularで簡易的なWebアプリを作っており、Auth0を使っています。

前回は、Cloudflare Workersを利用する方針で進めておりました。しかし、もろもろ検討した結果、Golang GinでAPIサーバーを起動することにしました。

そのため、タイトルの通りですが、Golang Ginのミドルウェアで、Auth0のJWTを検証したので、その際の記録です。

私は、最近Golangを勉強し始めた程度ですので、間違っていたり、もっと良い書き方があれば、コメント頂ければと思います。

Auth0やAngularの準備

前回の記事に記載しておりますので、省略します。

Cloudflare WorkersでAuth0のJWTを検証する | 株式会社フルーデンス

Golang Ginの準備

auth0/go-jwt-middleware: A Middleware for Go Programming Language to check for JWTs on HTTP requests

package main

import (
	"context"
	"log"
	"net/http"
	"net/url"
	"os"
	"time"

	jwtmiddleware "github.com/auth0/go-jwt-middleware/v2"
	"github.com/auth0/go-jwt-middleware/v2/jwks"
	"github.com/auth0/go-jwt-middleware/v2/validator"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	adapter "github.com/gwatts/gin-adapter"
)

// 環境変数から取得、なければデフォルト値
var (
	Auth0Domain   = os.Getenv("AUTH0_DOMAIN")
	Auth0Audience = os.Getenv("AUTH0_AUDIENCE")
)

type CustomClaims struct {
	Scope       string   `json:"scope"`
	Permissions []string `json:"permissions"`
}

// Validate はCustomClaimsインターフェースの実装に必要です
// 構造体のコピーを避けるため、ポインタレシーバにします
func (c *CustomClaims) Validate(ctx context.Context) error {
	return nil
}

// SetupAuth0Middleware はGin用のハンドラーを返します
func SetupAuth0Middleware() gin.HandlerFunc {
	issuerURL, err := url.Parse("https://" + Auth0Domain + "/")
	if err != nil {
		log.Fatalf("failed to parse issuer URL: %v", err)
	}

	provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)

	jwtValidator, err := validator.New(
		provider.KeyFunc,
		validator.RS256,
		issuerURL.String(),
		[]string{Auth0Audience},
		validator.WithCustomClaims(
			func() validator.CustomClaims {
				return &CustomClaims{}
			},
		),
		validator.WithAllowedClockSkew(time.Minute),
	)
	if err != nil {
		log.Fatalf("failed to initialize JWT validator: %v", err)
	}

	errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
		log.Printf("Encountered error while validating JWT: %v", err)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusUnauthorized)
		w.Write([]byte(`{"error":"JWT validation failed"}`))
	}

	middleware := jwtmiddleware.New(
		jwtValidator.ValidateToken,
		jwtmiddleware.WithErrorHandler(errorHandler),
	)

	// gwatts/gin-adapter を使うと標準のhttpミドルウェアをGin用に1行で変換できます
	return adapter.Wrap(middleware.CheckJWT)
}

func main() {
	// 動作確認用フォールバック(本来は環境変数で設定)
	if Auth0Domain == "" {
		Auth0Domain = "tenant-pq9.jp.auth0.com"
	}
	if Auth0Audience == "" {
		Auth0Audience = "https://api-endpoint-u5x/"
	}

	r := gin.Default()

	// CORS設定
	r.Use(cors.New(cors.Config{
		// 許可するオリジン (AngularのURL)
		AllowOrigins: []string{
			"http://localhost:4200",
		},
		// 許可するメソッド
		AllowMethods: []string{
			"GET", "POST", "PUT", "DELETE", "OPTIONS",
		},
		// 許可するヘッダー
		// Auth0を利用する場合、"Authorization" が必須です
		AllowHeaders: []string{
			"Authorization",
			"Content-Type",
		},
		// クッキーなどを送る場合に必要
		AllowCredentials: true,
		// プリフライトリクエストの結果をキャッシュする時間
		MaxAge: 12 * time.Hour,
	}))

	// Auth0 ミドルウェアを設定
	r.Use(SetupAuth0Middleware())

	// 独自のクレーム抽出用ミドルウェア
	// context.Context (標準) から gin.Context へ値を移し替える
	r.Use(func(c *gin.Context) {
		token := c.Request.Context().Value(jwtmiddleware.ContextKey{})
		if token != nil {
			validatedClaims := token.(*validator.ValidatedClaims)
			customClaims := validatedClaims.CustomClaims.(*CustomClaims)

			// Gin Contextに保存して後続のハンドラーで使いやすくする
			c.Set("permissions", customClaims.Permissions)
			c.Set("registeredClaims", validatedClaims.RegisteredClaims)
		}
		c.Next()
	})

	r.GET("/verify", func(c *gin.Context) {
		// c.MustGetを使うと存在しない場合にpanicするので、安全にGetを使う
		claims, exists := c.Get("registeredClaims")
		if !exists {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "claims not found"})
			return
		}

		registeredClaims := claims.(validator.RegisteredClaims)

		c.JSON(http.StatusOK, gin.H{
			"subject": registeredClaims.Subject,
		})
	})

	if err := r.Run(":8080"); err != nil {
		log.Fatalf("failed to run server: %v", err)
	}
}
main.go:Auth0のJWT検証を行うGinミドルウェアの実装

サーバーを起動します。

トークンをセットしないで、リクエストします。

curl -X GET "http://localhost:8080/verify" | jq -r
JWTトークンなしでリクエストを実行
teruhiro:~/go/src/github.com/terukomaki/go-gin-auth0-hf3 $ curl -X GET "http://localhost:8080/verify" | jq -r
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    33  100    33    0     0  48672      0 --:--:-- --:--:-- --:--:-- 33000
{
  "error": "JWT validation failed"
}
実行結果:認証エラー (401 Unauthorized) が返る

トークンをセットして、リクエストします。

TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InBZcWxja0pWaUI5NHlLU1oySzBVVyJ9.eyJpc3MiOiJodHRwczovL3RlbmFudC1wcTkuanAuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDY3NGZiZmU3OTcwMjE1ZGE3MzQ3NzMwOCIsImF1ZCI6WyJodHRwczovL2FwaS1lbmRwb2ludC11NXgvIiwiaHR0cHM6Ly90ZW5hbnQtcHE5LmpwLmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE3MzQ0NDY4NjEsImV4cCI6MTczNDUzMzI2MSwic2NvcGUiOiJvcGVuaWQiLCJhenAiOiJydlp6aTB1NFQyRXg3SVhlR2pId2xZWjVadDlNUzk2USIsInBlcm1pc3Npb25zIjpbXX0.sinBFBHl2CFnyph-0l0Z_iSCqejcfAoA4iVOxvyH6mcyUvcOgFlhB52GL9kn_aw6RJWv8u_Vm_DJsdxfq1wcQL8OV4YgGkMZjfw0Y2LB4otRSVBjHYXf0UL1mi8cnWLn9oXM5nckT3KgjsJGgOx-p2p4uUcn55qjj_lqRYpmGbb3bToB2JqLA3-unXLP4A_o57bzqHJXNDxNPScuBzLt1azizaFAHcGWsoj_y98NeLqYRGx6sDqgdllStzBHREKVar2FUq5Rh5CvaH2LltneI2jOqH6w3kpXqKUUfyYPwsCc8Rw_CoCOEMTiigyxFTYahUJQfkgv133V3F_eD7LCag

curl -X GET -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/verify" | jq -r
Auth0から取得したJWTトークンをヘッダーにセットしてリクエスト
teruhiro:~/go/src/github.com/terukomaki/go-gin-auth0-hf3 $ TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InBZcWxja0pWaUI5NHlLU1oySzBVVyJ9.eyJpc3MiOiJodHRwczovL3RlbmFudC1wcTkuanAuYXV0aDAuY29tLyIsInN1YiI6ImF1dGgwfDY3NGZiZmU3OTcwMjE1ZGE3MzQ3NzMwOCIsImF1ZCI6WyJodHRwczovL2FwaS1lbmRwb2ludC11NXgvIiwiaHR0cHM6Ly90ZW5hbnQtcHE5LmpwLmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE3MzQ0NDY4NjEsImV4cCI6MTczNDUzMzI2MSwic2NvcGUiOiJvcGVuaWQiLCJhenAiOiJydlp6aTB1NFQyRXg3SVhlR2pId2xZWjVadDlNUzk2USIsInBlcm1pc3Npb25zIjpbXX0.sinBFBHl2CFnyph-0l0Z_iSCqejcfAoA4iVOxvyH6mcyUvcOgFlhB52GL9kn_aw6RJWv8u_Vm_DJsdxfq1wcQL8OV4YgGkMZjfw0Y2LB4otRSVBjHYXf0UL1mi8cnWLn9oXM5nckT3KgjsJGgOx-p2p4uUcn55qjj_lqRYpmGbb3bToB2JqLA3-unXLP4A_o57bzqHJXNDxNPScuBzLt1azizaFAHcGWsoj_y98NeLqYRGx6sDqgdllStzBHREKVar2FUq5Rh5CvaH2LltneI2jOqH6w3kpXqKUUfyYPwsCc8Rw_CoCOEMTiigyxFTYahUJQfkgv133V3F_eD7LCag
teruhiro:~/go/src/github.com/terukomaki/go-gin-auth0-hf3 $ curl -X GET -H "Authorization: Bearer $TOKEN" \
> "http://localhost:8080/verify" | jq -r
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    44  100    44    0     0    131      0 --:--:-- --:--:-- --:--:--   131
{
  "subject": "auth0|674fbfe7970215da73477308"
}
実行結果:認証に成功し、subject (sub) が取得できる

無事、JWTが検証でき、デコードしてsubjectが取得できました。

参考情報

小巻旭洋のプロフィール写真

小巻 旭洋

2014年からフリーランスとして活動し、2016年に株式会社フルーデンスを設立する。FileMaker開発歴は約10年。多数の企業システム構築を手がけ、特にデータベース設計と、JavaScript連携、Web連携、パフォーマンス最適化を専門としています。2025年から、Web業務システム・アプリ開発をメインに開発をしています。