* fix(HH-548): authorize super admin platform lists * fix(HH-548): limit dual auth to platform lists --------- Co-authored-by: Rogee <rogee@ipao.vip>
128 lines
4.4 KiB
Go
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()
|
|
}
|
|
}
|