Files
gochat/backend/internal/middleware/platform_app_auth.go
T
Rogeeandrogee 8d5d019bb5 HH-548: allow Super Admin sessions to load platform lists (#125)
* fix(HH-548): authorize super admin platform lists

* fix(HH-548): limit dual auth to platform lists

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-23 20:21:05 +08:00

128 lines
4.4 KiB
Go

package middleware
// Reference: Chatwoot PlatformController — AccessToken authentication
// Chatwoot authenticates Platform API requests via the api_access_token header,
// which maps to a PlatformApp owner. This middleware replicates that pattern:
//
// 1. Read api_access_token from request headers
// 2. Look up the AccessToken record (token is SHA-256 hashed in DB)
// 3. Verify the owner is a PlatformApp (not a User personal token)
// 4. Set platform_app in Gin context for downstream handlers
//
// Permissible validation is handled per-resource in each handler,
// matching Chatwoot's validate_platform_app_permissible callback.
import (
"crypto/sha256"
"encoding/hex"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/auth"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/pkg/response"
"gorm.io/gorm"
)
// PlatformAPIIdentityAuth accepts either a PlatformApp credential or the
// signed dashboard session used by GoChat's Super Admin SPA. An explicitly
// supplied PlatformApp credential never falls back to a user session.
func PlatformAPIIdentityAuth(jwtSvc *auth.JWTService, db *gorm.DB) gin.HandlerFunc {
platformAppAuth := PlatformAppAuth(db)
userAuth := AuthMiddlewareWithServiceAndDB(jwtSvc, db)
return func(c *gin.Context) {
if strings.TrimSpace(c.GetHeader("api_access_token")) != "" || strings.TrimSpace(c.GetHeader("HTTP_API_ACCESS_TOKEN")) != "" {
platformAppAuth(c)
return
}
userAuth(c)
}
}
// PlatformAPIAuthorize limits user sessions to Super Admins while preserving
// the existing PlatformApp authentication and permissible checks.
func PlatformAPIAuthorize() gin.HandlerFunc {
superAdmin := SuperAdmin()
return func(c *gin.Context) {
if _, ok := c.Get("platform_app_id"); ok {
c.Next()
return
}
superAdmin(c)
}
}
// PlatformAppAuth creates a middleware that authenticates Platform API requests
// via the api_access_token header (Chatwoot-compatible).
//
// The middleware:
// - Extracts api_access_token from request headers
// - Hashes the token (SHA-256) and queries the access_tokens table
// - Verifies the token owner is a PlatformApp
// - Loads the PlatformApp with its Permissibles for downstream checks
// - Sets platform_app_id and platform_app in Gin context
//
// If no token is provided, or the token is invalid, or the owner is not
// a PlatformApp, the request is rejected with 401 Unauthorized.
func PlatformAppAuth(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// Step 1: Extract api_access_token from headers
// Chatwoot reads both :api_access_token and :HTTP_API_ACCESS_TOKEN
token := c.GetHeader("api_access_token")
if token == "" {
token = c.GetHeader("HTTP_API_ACCESS_TOKEN")
}
if token == "" {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
"api_access_token header required")
return
}
// Step 2: Hash the token (SHA-256) to match stored hash
hash := sha256.Sum256([]byte(token))
tokenHash := hex.EncodeToString(hash[:])
// Step 3: Query AccessToken by hashed token
var accessToken model.AccessToken
if err := db.Where("token = ? AND owner_type = ?", tokenHash, model.AccessTokenOwnerTypePlatformApp).
First(&accessToken).Error; err != nil {
if err == gorm.ErrRecordNotFound {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
"Invalid access_token")
return
}
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal,
"Failed to verify access token")
return
}
if accessToken.ExpiresAt != nil && !accessToken.ExpiresAt.After(time.Now()) {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
"Invalid access_token")
return
}
// Step 4: Load the PlatformApp with its Permissibles
var platformApp model.PlatformApp
if err := db.Preload("Permissibles").First(&platformApp, accessToken.OwnerID).Error; err != nil {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
"Invalid access_token")
return
}
if !platformApp.IsActive() {
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized,
"Invalid access_token")
return
}
// Step 5: Set platform_app in Gin context for downstream handlers
c.Set("platform_app", platformApp)
c.Set("platform_app_id", platformApp.ID)
c.Set("access_token_id", accessToken.ID)
c.Next()
}
}