fix: harden identity, live access, drafts and setup (review only)

Source-reviewed hardening changes and regression tests. UNBUILT / UNTESTED: not a deployable release. Go formatting, compilation, automated regression tests, full frontend validation and staging acceptance remain required. See docs/hardening/VALIDATION.md. Main remains the original clean snapshot; no upstream Git history imported.
This commit is contained in:
2026-10-16 12:15:20 +00:00
parent e57610a27c
commit 63e0457886
86 changed files with 5174 additions and 1929 deletions

View File

@@ -1,7 +1,11 @@
// Modified in the independent hardening fork; see docs/hardening.
package services
import (
"errors"
"crypto/rand"
"encoding/hex"
"strings"
"time"
@@ -42,10 +46,13 @@ type customerSessionClaims struct {
CustomerID int64 `json:"customerId"`
CustomerName string `json:"customerName"`
IdentityKey string `json:"identityKey"`
IdentityScope string `json:"identityScope"`
jwt.RegisteredClaims
}
type CustomerSessionVerifyResult struct {
CustomerID int64
ChannelID int64
ExternalUser *openidentity.ExternalUser
Token string
ExpiresAt time.Time
@@ -56,7 +63,18 @@ func (s *customerSessionService) Exchange(channel *models.Channel, externalUser
if channel == nil || channel.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0209")
}
var customerID int64
if !openidentity.ValidIdentityScope(channel.ChannelID, externalUser) || (ChannelService.RequiresSignedIdentity(channel) && externalUser.ExternalSource != enums.ExternalSourceUser) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
externalUser.ChannelID = channel.ID
if externalUser.ExternalSource == enums.ExternalSourceGuest {
// A public caller's guest ID is not proof of possession. Only the signed
// customer session can resume this server-issued anonymous identity.
var id [32]byte
if _, err := rand.Read(id[:]); err != nil { return nil, err }
externalUser.ExternalID = hex.EncodeToString(id[:])
}
var customerID int64
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
id, err := CustomerService.EnsureExternalCustomer(ctx, externalUser)
if err != nil {
@@ -68,10 +86,12 @@ func (s *customerSessionService) Exchange(channel *models.Channel, externalUser
return nil, err
}
customer := CustomerService.Get(customerID)
if customer == nil || customer.Status == enums.StatusDeleted {
if customer == nil || customer.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0155")
}
token, expiresAt, err := s.Sign(channel, customer, externalUser)
identity, identityErr := repositories.CustomerIdentityRepository.GetUniqueBy(sqls.DB(), externalUser.IdentityScope, externalUser.ExternalSource, externalUser.ExternalID)
if identityErr != nil || identity == nil || identity.CustomerID != customer.ID { return nil, errorsx.UnauthorizedI18n("error.e0161") }
token, expiresAt, err := s.Sign(channel, customer, externalUser)
if err != nil {
return nil, err
}
@@ -92,7 +112,7 @@ func (s *customerSessionService) Sign(channel *models.Channel, customer *models.
if secret == "" {
return "", time.Time{}, errorsx.BusinessErrorI18n(1, "error.customerSession.secretMissing")
}
if channel == nil || customer == nil {
if channel == nil || customer == nil || !openidentity.ValidIdentityScope(channel.ChannelID, externalUser) {
return "", time.Time{}, errorsx.InvalidParamI18n("error.e0158")
}
now := time.Now()
@@ -104,6 +124,7 @@ func (s *customerSessionService) Sign(channel *models.Channel, customer *models.
CustomerID: customer.ID,
CustomerName: strings.TrimSpace(customer.Name),
IdentityKey: s.identityKey(externalUser),
IdentityScope: externalUser.IdentityScope,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(expiresAt),
@@ -125,27 +146,11 @@ func (s *customerSessionService) VerifyRequest(ctx *gin.Context, channel *models
if err != nil {
return nil, err
}
if channel == nil || channel.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0209")
}
if claims.ChannelID != channel.ID || strings.TrimSpace(claims.ChannelCode) != strings.TrimSpace(channel.ChannelID) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
customer := CustomerService.Get(claims.CustomerID)
if customer == nil || customer.Status == enums.StatusDeleted {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
external, err := s.externalUserFromClaims(claims, customer)
if err != nil {
return nil, err
}
result := &CustomerSessionVerifyResult{
ExternalUser: external,
Token: token,
ExpiresAt: claims.ExpiresAt.Time,
}
result, err := s.verifyClaims(claims, channel)
if err != nil { return nil, err }
result.Token = token
if s.shouldRefresh(claims.ExpiresAt.Time) {
newToken, expiresAt, err := s.Sign(channel, customer, *external)
newToken, expiresAt, err := s.Sign(channel, CustomerService.Get(result.CustomerID), *result.ExternalUser)
if err != nil {
return nil, err
}
@@ -190,7 +195,7 @@ func (s *customerSessionService) verifyToken(rawToken string) (*customerSessionC
if token == nil || !token.Valid || claims.TokenType != customerSessionTokenType || claims.ExpiresAt == nil {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
if claims.ChannelID <= 0 || strings.TrimSpace(claims.ChannelCode) == "" || claims.CustomerID <= 0 || strings.TrimSpace(claims.IdentityKey) == "" {
if claims.ChannelID <= 0 || strings.TrimSpace(claims.ChannelCode) == "" || claims.CustomerID <= 0 || strings.TrimSpace(claims.IdentityKey) == "" || strings.TrimSpace(claims.IdentityScope) == "" {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
return claims, nil
@@ -211,8 +216,8 @@ func (s *customerSessionService) externalUserFromClaims(claims *customerSessionC
default:
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), source, parts[1])
if identity == nil || identity.CustomerID != claims.CustomerID {
identity, err := repositories.CustomerIdentityRepository.GetUniqueBy(sqls.DB(), claims.IdentityScope, source, parts[1])
if err != nil || identity == nil || identity.CustomerID != claims.CustomerID {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
name := strings.TrimSpace(claims.CustomerName)
@@ -223,6 +228,8 @@ func (s *customerSessionService) externalUserFromClaims(claims *customerSessionC
ExternalSource: source,
ExternalID: parts[1],
ExternalName: name,
IdentityScope: claims.IdentityScope,
ChannelID: claims.ChannelID,
}, nil
}
@@ -250,3 +257,30 @@ func (s *customerSessionService) getCustomerSessionToken(ctx *gin.Context) strin
token, _ := params.Get(ctx, "customerSessionToken")
return strings.TrimSpace(token)
}
// VerifyConnection rechecks the signed credential, expiry, channel policy and
// current identity mapping. It never refreshes an idle connection indefinitely.
func (s *customerSessionService) VerifyConnection(result *CustomerSessionVerifyResult) bool {
if result == nil || !time.Now().Before(result.ExpiresAt) { return false }
claims, err := s.verifyToken(result.Token)
if err != nil || claims.CustomerID != result.CustomerID || claims.ChannelID != result.ChannelID { return false }
verified, err := s.verifyClaims(claims, ChannelService.Get(claims.ChannelID))
return err == nil && verified.ExternalUser != nil && result.ExternalUser != nil &&
verified.ExternalUser.IdentityScope == result.ExternalUser.IdentityScope &&
verified.ExternalUser.ExternalSource == result.ExternalUser.ExternalSource &&
verified.ExternalUser.ExternalID == result.ExternalUser.ExternalID
}
func (s *customerSessionService) verifyClaims(claims *customerSessionClaims, channel *models.Channel) (*CustomerSessionVerifyResult, error) {
if channel == nil || channel.Status != enums.StatusOk || claims.ChannelID != channel.ID || strings.TrimSpace(claims.ChannelCode) != strings.TrimSpace(channel.ChannelID) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
customer := CustomerService.Get(claims.CustomerID)
if customer == nil || customer.Status != enums.StatusOk { return nil, errorsx.UnauthorizedI18n("error.e0161") }
external, err := s.externalUserFromClaims(claims, customer)
if err != nil { return nil, err }
if !openidentity.ValidIdentityScope(channel.ChannelID, *external) || (ChannelService.RequiresSignedIdentity(channel) && external.ExternalSource != enums.ExternalSourceUser) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
return &CustomerSessionVerifyResult{CustomerID: customer.ID, ChannelID: channel.ID, ExternalUser: external, ExpiresAt: claims.ExpiresAt.Time}, nil
}