---
date: 2024-12-18 00:30:00
description: 簡易的なWebアプリでCloudflare Workersを利用しておりましたが、Golang Ginに変更しました。そのため、GinでAuth0のJWTを検証しました。
title: Golang GinでAuth0のJWTを検証する
updatedDate: 2025-12-16 18:00:00
---

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

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

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

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

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

## Auth0やAngularの準備

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

[Cloudflare WorkersでAuth0のJWTを検証する | 株式会社フルーデンス](https://www.frudens.com/blog/2024/12/17/cloudflare-workers-auth0-jwt-validation/)

## Golang Ginの準備

[auth0/go-jwt-middleware: A Middleware for Go Programming Language to check for JWTs on HTTP requests](https://github.com/auth0/go-jwt-middleware)


```go
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ミドルウェアの実装

サーバーを起動します。

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


```shell
curl -X GET "http://localhost:8080/verify" | jq -r
```

  JWTトークンなしでリクエストを実行


```shell
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) が返る

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


```shell
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トークンをヘッダーにセットしてリクエスト


```shell
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`が取得できました。

## 参考情報

- [🌐 Golang RESTful API with Gin, Gorm, PostgreSQL 🐘 - DEV Community](https://dev.to/truongpx396/golang-restful-api-with-gin-gorm-postgresql-2hc)
- [Auth0 JWT Middleware in Go - Gin Web Framework - DEV Community](https://dev.to/ksivamuthu/auth0-jwt-middleware-in-go-gin-web-framework-37mj)
- [Auth0 Go API SDK Quickstarts: Add authorization to a Go API](https://auth0.com/docs/quickstart/backend/golang/interactive)
- [gwatts/gin-adapter: Adapt standard Go HTTP middleware packages for use with Gin](https://github.com/gwatts/gin-adapter)