package auth import ( "context" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" "git.118im.com/im_team/im_workbench/internal/config" "github.com/redis/go-redis/v9" ) var ( ErrAuthRequired = errors.New("authentication required") ErrForbidden = errors.New("forbidden") ) type Principal struct { TenantID int64 TenantCode string UserID int64 DeviceID string AuthLevel string } type Session struct { UserID int64 `json:"user_id"` TenantID int64 `json:"tenant_id"` TenantCode string `json:"tenant_code"` DeviceID string `json:"device_id"` AuthLevel string `json:"auth_level"` State string `json:"state"` ExpiresAt string `json:"expires_at"` IssuedAt string `json:"issued_at"` } type Authenticator struct { cfg config.AuthConfig redis *redis.Client } func New(cfg config.AuthConfig, redisClient *redis.Client) *Authenticator { return &Authenticator{cfg: cfg, redis: redisClient} } func (a *Authenticator) Authenticate(req *http.Request) (*Principal, error) { if err := a.validateAPIKey(req); err != nil { return nil, err } if a.cfg.AllowDevHeaderAuth { if principal := principalFromDevHeaders(req); principal != nil { return principal, nil } } if a.cfg.SkipSessionInDev { return &Principal{ TenantID: parseInt64Header(req, "X-Tenant-ID", 1), TenantCode: firstNonEmpty(req.Header.Get("X-Tenant-Code"), "default"), UserID: parseInt64Header(req, "X-User-ID", 1), AuthLevel: "auth", }, nil } if a.redis == nil { return nil, ErrAuthRequired } token, err := bearerToken(req) if err != nil { return nil, err } session, err := a.sessionFromRedis(req.Context(), token) if err != nil { return nil, err } if session.UserID <= 0 || session.TenantID <= 0 { return nil, ErrAuthRequired } if state := strings.ToLower(strings.TrimSpace(session.State)); state != "" && state != "active" { return nil, ErrAuthRequired } if session.ExpiresAt != "" { expiresAt, err := time.Parse(time.RFC3339, session.ExpiresAt) if err == nil && time.Now().After(expiresAt) { return nil, ErrAuthRequired } } headerTenant := strings.TrimSpace(req.Header.Get("X-Tenant-Code")) if a.cfg.RequiredTenantCode && headerTenant == "" { return nil, ErrForbidden } if headerTenant != "" && session.TenantCode != "" && headerTenant != session.TenantCode { return nil, ErrForbidden } return &Principal{ TenantID: session.TenantID, TenantCode: firstNonEmpty(session.TenantCode, headerTenant), UserID: session.UserID, DeviceID: session.DeviceID, AuthLevel: session.AuthLevel, }, nil } func (a *Authenticator) validateAPIKey(req *http.Request) error { if len(a.cfg.APIKeys) == 0 && !a.cfg.RequiredAPIKey { return nil } got := strings.TrimSpace(req.Header.Get("X-MeetingIM-APIKey")) if got == "" { got = strings.TrimSpace(req.URL.Query().Get("apikey")) } if got == "" { if a.cfg.RequiredAPIKey { return ErrForbidden } return nil } if len(a.cfg.APIKeys) == 0 { return nil } for _, key := range a.cfg.APIKeys { if subtle.ConstantTimeCompare([]byte(got), []byte(strings.TrimSpace(key))) == 1 { return nil } } return ErrForbidden } func (a *Authenticator) sessionFromRedis(ctx context.Context, token string) (*Session, error) { secret := []byte(strings.TrimSpace(token)) if decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(token)); err == nil { secret = decoded } digest := sha256.Sum256(secret) key := fmt.Sprintf(a.cfg.SessionKeyPattern, hex.EncodeToString(digest[:])) data, err := a.redis.Get(ctx, key).Bytes() if err != nil && errors.Is(err, redis.Nil) { return nil, ErrAuthRequired } if err != nil { return nil, err } var session Session if err := json.Unmarshal(data, &session); err != nil { return nil, ErrAuthRequired } return &session, nil } func bearerToken(req *http.Request) (string, error) { authz := strings.TrimSpace(req.Header.Get("Authorization")) token, ok := strings.CutPrefix(authz, "Bearer ") if !ok || strings.TrimSpace(token) == "" { return "", ErrAuthRequired } return strings.TrimSpace(token), nil } func principalFromDevHeaders(req *http.Request) *Principal { uid := parseInt64Header(req, "X-User-ID", 0) if uid <= 0 { return nil } return &Principal{ TenantID: parseInt64Header(req, "X-Tenant-ID", 1), TenantCode: firstNonEmpty(req.Header.Get("X-Tenant-Code"), "default"), UserID: uid, DeviceID: strings.TrimSpace(req.Header.Get("X-Device-ID")), AuthLevel: "auth", } } func parseInt64Header(req *http.Request, key string, fallback int64) int64 { value := strings.TrimSpace(req.Header.Get(key)) if value == "" { return fallback } parsed, err := strconv.ParseInt(value, 10, 64) if err != nil { return fallback } return parsed } func firstNonEmpty(values ...string) string { for _, value := range values { if trimmed := strings.TrimSpace(value); trimmed != "" { return trimmed } } return "" }