172 lines
4.4 KiB
Go
172 lines
4.4 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/net/ghttp"
|
|
|
|
"github.com/qianfree/team-api/internal/consts"
|
|
"github.com/qianfree/team-api/internal/dao"
|
|
"github.com/qianfree/team-api/internal/model/entity"
|
|
"github.com/qianfree/team-api/internal/response"
|
|
"github.com/qianfree/team-api/internal/utility/crypto"
|
|
)
|
|
|
|
const (
|
|
openPlatformHeaderSignature = "X-Signature"
|
|
openPlatformHeaderTimestamp = "X-Timestamp"
|
|
openPlatformHeaderNonce = "X-Nonce"
|
|
openPlatformHeaderAppID = "X-App-Id"
|
|
openPlatformMaxSkew = 5 * time.Minute
|
|
)
|
|
|
|
// OpenPlatformAuth is the HMAC-SHA256 authentication middleware for /open/* endpoints.
|
|
func OpenPlatformAuth(r *ghttp.Request) {
|
|
appID := r.GetHeader(openPlatformHeaderAppID)
|
|
signature := r.GetHeader(openPlatformHeaderSignature)
|
|
timestampStr := r.GetHeader(openPlatformHeaderTimestamp)
|
|
nonce := r.GetHeader(openPlatformHeaderNonce)
|
|
|
|
if appID == "" || signature == "" || timestampStr == "" || nonce == "" {
|
|
response.Error(r, consts.ErrUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Validate timestamp skew
|
|
ts, err := strconv.ParseInt(timestampStr, 10, 64)
|
|
if err != nil {
|
|
response.Error(r, consts.ErrUnauthorized)
|
|
return
|
|
}
|
|
if time.Since(time.Unix(ts, 0)).Abs() > openPlatformMaxSkew {
|
|
response.Error(r, consts.ErrUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Lookup app by app_id
|
|
var app entity.OpnApps
|
|
err = dao.OpnApps.Ctx(r.Context()).Where("app_id", appID).Scan(&app)
|
|
if err != nil || app.Id == 0 {
|
|
response.Error(r, consts.ErrUnauthorized)
|
|
return
|
|
}
|
|
|
|
if app.Status != "active" {
|
|
response.Error(r, consts.ErrUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Check IP whitelist
|
|
if err := checkOpenAppIPWhitelist(r, app); err != nil {
|
|
response.Error(r, err)
|
|
return
|
|
}
|
|
|
|
// Get app secret from Redis
|
|
secret, err := getOpenAppSecret(r.Context(), app.Id)
|
|
if err != nil || secret == "" {
|
|
response.Error(r, consts.ErrUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Verify HMAC signature
|
|
expectedSig := computeHMACSignature(secret, timestampStr, nonce, r.Method, r.URL.Path)
|
|
if !hmac.Equal([]byte(signature), []byte(expectedSig)) {
|
|
response.Error(r, consts.ErrUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Inject context variables
|
|
ctx := r.Context()
|
|
ctx = context.WithValue(ctx, "openAppId", app.Id)
|
|
ctx = context.WithValue(ctx, "openTenantId", app.TenantId)
|
|
ctx = context.WithValue(ctx, "openPermissions", app.Permissions)
|
|
ctx = context.WithValue(ctx, "openIsSandbox", app.IsSandbox)
|
|
r.SetCtx(ctx)
|
|
|
|
r.Middleware.Next()
|
|
}
|
|
|
|
func computeHMACSignature(secret, timestamp, nonce, method, path string) string {
|
|
message := fmt.Sprintf("%s\n%s\n%s\n%s", timestamp, nonce, method, path)
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(message))
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func checkOpenAppIPWhitelist(r *ghttp.Request, app entity.OpnApps) error {
|
|
if app.IpWhitelist == "" || app.IpWhitelist == "[]" {
|
|
return nil
|
|
}
|
|
var whitelist []string
|
|
_ = json.Unmarshal([]byte(app.IpWhitelist), &whitelist)
|
|
if len(whitelist) == 0 {
|
|
return nil
|
|
}
|
|
|
|
clientIP := r.GetClientIp()
|
|
for _, allowed := range whitelist {
|
|
if clientIP == allowed {
|
|
return nil
|
|
}
|
|
if strings.HasSuffix(allowed, ".*") {
|
|
prefix := strings.TrimSuffix(allowed, ".*")
|
|
if strings.HasPrefix(clientIP, prefix) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return consts.ErrIpRestricted
|
|
}
|
|
|
|
func getOpenAppSecret(ctx context.Context, appID int64) (string, error) {
|
|
val, err := g.Redis().Do(ctx, "GET", fmt.Sprintf("open:secret:%d", appID))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if val.IsNil() || val.IsEmpty() {
|
|
return "", fmt.Errorf("secret not found")
|
|
}
|
|
encKey := getEncKey(ctx)
|
|
return crypto.DecryptString(encKey, val.String())
|
|
}
|
|
|
|
func getEncKey(ctx context.Context) []byte {
|
|
hexKey := g.Cfg().MustGet(ctx, "crypto.encryptionKey").String()
|
|
if hexKey == "" {
|
|
hexKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
}
|
|
return crypto.MustGetEncryptionKey(hexKey)
|
|
}
|
|
|
|
// GetOpenTenantID extracts the tenant ID injected by OpenPlatformAuth middleware.
|
|
func GetOpenTenantID(ctx context.Context) int64 {
|
|
val := ctx.Value("openTenantId")
|
|
if val != nil {
|
|
if id, ok := val.(int64); ok {
|
|
return id
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// GetOpenAppID extracts the app ID injected by OpenPlatformAuth middleware.
|
|
func GetOpenAppID(ctx context.Context) int64 {
|
|
val := ctx.Value("openAppId")
|
|
if val != nil {
|
|
if id, ok := val.(int64); ok {
|
|
return id
|
|
}
|
|
}
|
|
return 0
|
|
}
|