Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
"github.com/fun-dotto/admin-bff-api/internal/infrastructure"
"github.com/fun-dotto/admin-bff-api/internal/middleware"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
oapimiddleware "github.com/oapi-codegen/gin-middleware"
"github.com/oapi-codegen/gin-middleware"
)

func main() {
Expand All @@ -39,8 +40,18 @@ func main() {

router := gin.Default()

router.Use(oapimiddleware.OapiRequestValidator(spec))
router.Use(middleware.FirebaseAuth(authClient))
router.Use(ginmiddleware.OapiRequestValidatorWithOptions(spec, &ginmiddleware.Options{
ErrorHandler: func(c *gin.Context, message string, statusCode int) {
if authStatusCode, authMessage, ok := middleware.GetAuthenticationError(c); ok {
c.AbortWithStatusJSON(authStatusCode, gin.H{"error": authMessage})
return
}
c.AbortWithStatusJSON(statusCode, gin.H{"error": message})
},
Options: openapi3filter.Options{
AuthenticationFunc: middleware.FirebaseAuthenticationFunc(authClient),
},
}))

clients, err := infrastructure.NewExternalClients(ctx)
if err != nil {
Expand Down
139 changes: 112 additions & 27 deletions internal/middleware/firebase_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,145 @@ package middleware

import (
"context"
"errors"
"net/http"
"strings"

"firebase.google.com/go/v4/auth"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/gin-gonic/gin"
ginmiddleware "github.com/oapi-codegen/gin-middleware"
)

type firebaseTokenKey struct{}
type authErrorStatusKey struct{}
type authErrorMessageKey struct{}

// FirebaseTokenContextKey は Gin の context および context.Context に
// Firebase ID トークンの検証結果を格納するキーです。
var FirebaseTokenContextKey = firebaseTokenKey{}

var (
authenticationErrorStatusKey = authErrorStatusKey{}
authenticationErrorMessageKey = authErrorMessageKey{}
)

type AuthenticationError struct {
StatusCode int
Message string
}

func (e *AuthenticationError) Error() string {
return e.Message
}

// FirebaseAuthenticationFunc は OpenAPI validator 向けの AuthenticationFunc を返します。
// 認証に成功した場合は検証済みトークンを Gin / request context に格納します。
func FirebaseAuthenticationFunc(authClient *auth.Client) openapi3filter.AuthenticationFunc {
return func(ctx context.Context, _ *openapi3filter.AuthenticationInput) error {
ginCtx := ginmiddleware.GetGinContext(ctx)
if ginCtx == nil {
return &AuthenticationError{
StatusCode: http.StatusUnauthorized,
Message: "Authentication context is unavailable",
Comment on lines +44 to +45
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When GetGinContext(ctx) returns nil, this indicates a server/middleware wiring issue rather than an authentication failure. Returning 401 here can mislead clients and hide operational problems; consider treating this as an internal error (e.g., 500) and/or ensuring the error handler does not expose this internal message to clients.

Suggested change
StatusCode: http.StatusUnauthorized,
Message: "Authentication context is unavailable",
StatusCode: http.StatusInternalServerError,
Message: "Internal authentication error",

Copilot uses AI. Check for mistakes.
}
}

token, err := verifyFirebaseToken(ginCtx.GetHeader("Authorization"), ginCtx.Request.Context(), authClient)
if err != nil {
var authErr *AuthenticationError
if errors.As(err, &authErr) {
ginCtx.Set(authenticationErrorStatusKey, authErr.StatusCode)
ginCtx.Set(authenticationErrorMessageKey, authErr.Message)
}
return err
}

setFirebaseToken(ginCtx, token)
return nil
}
}

// FirebaseAuth は Authorization: Bearer <Firebase ID Token> を検証する Gin ミドルウェアです。
// 検証に成功すると、デコードされたトークン(*auth.Token)を context に格納して次のハンドラに渡します。
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FirebaseAuth middleware is no longer referenced anywhere in this repository (the server now wires auth via the OpenAPI validator). Consider removing this middleware or clearly deprecating it to avoid confusion about which auth path is active.

Suggested change
// 検証に成功すると、デコードされたトークン(*auth.Token)を context に格納して次のハンドラに渡します。
// 検証に成功すると、デコードされたトークン(*auth.Token)を context に格納して次のハンドラに渡します。
// Deprecated: このミドルウェアは現在サーバでは使用されていません。認証は OpenAPI validator を通じて
// FirebaseAuthenticationFunc を用いて行われます。新しいコードでは FirebaseAuth ではなく
// FirebaseAuthenticationFunc を利用してください。

Copilot uses AI. Check for mistakes.
func FirebaseAuth(authClient *auth.Client) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header is required",
})
token, err := verifyFirebaseToken(c.GetHeader("Authorization"), c.Request.Context(), authClient)
if err != nil {
var authErr *AuthenticationError
if errors.As(err, &authErr) {
c.AbortWithStatusJSON(authErr.StatusCode, gin.H{"error": authErr.Message})
return
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"})
return
Comment on lines +69 to 76
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback branch that returns a generic 401 ("Authentication failed") is effectively unreachable right now because verifyFirebaseToken always returns an *AuthenticationError on failure. Consider removing the dead branch, or change verifyFirebaseToken to return non-auth errors distinctly (e.g., wrap unexpected errors as 500) so this branch can be meaningful.

Copilot uses AI. Check for mistakes.
}

parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header must be Bearer <token>",
})
return
setFirebaseToken(c, token)
c.Next()
}
}

func verifyFirebaseToken(authHeader string, ctx context.Context, authClient *auth.Client) (*auth.Token, error) {
if authHeader == "" {
return nil, &AuthenticationError{
StatusCode: http.StatusUnauthorized,
Message: "Authorization header is required",
}
}

idToken := strings.TrimSpace(parts[1])
if idToken == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "ID token is required",
})
return
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return nil, &AuthenticationError{
StatusCode: http.StatusUnauthorized,
Message: "Authorization header must be Bearer <token>",
}
}

ctx := c.Request.Context()
token, err := authClient.VerifyIDToken(ctx, idToken)
if err != nil {
status, message := authErrorResponse(err)
c.AbortWithStatusJSON(status, gin.H{"error": message})
return
idToken := strings.TrimSpace(parts[1])
if idToken == "" {
return nil, &AuthenticationError{
StatusCode: http.StatusUnauthorized,
Message: "ID token is required",
}
}

c.Set(FirebaseTokenContextKey, token)
ctx = context.WithValue(ctx, FirebaseTokenContextKey, token)
c.Request = c.Request.WithContext(ctx)
c.Next()
token, err := authClient.VerifyIDToken(ctx, idToken)
if err != nil {
status, message := authErrorResponse(err)
return nil, &AuthenticationError{
StatusCode: status,
Message: message,
}
}

return token, nil
}

func setFirebaseToken(c *gin.Context, token *auth.Token) {
ctx := context.WithValue(c.Request.Context(), FirebaseTokenContextKey, token)
c.Set(FirebaseTokenContextKey, token)
c.Request = c.Request.WithContext(ctx)
}

// GetAuthenticationError は validator の AuthenticationFunc が格納した認証失敗情報を返します。
func GetAuthenticationError(c *gin.Context) (int, string, bool) {
status, statusExists := c.Get(authenticationErrorStatusKey)
message, messageExists := c.Get(authenticationErrorMessageKey)
if !statusExists || !messageExists {
return 0, "", false
}

statusCode, ok := status.(int)
if !ok {
return 0, "", false
}
errorMessage, ok := message.(string)
if !ok {
return 0, "", false
}

return statusCode, errorMessage, true
}

// authErrorResponse は Firebase Auth の検証エラーから HTTP ステータスとメッセージを返します。
Expand Down
Loading