H-337: restore Web widget reply visibility (#64)
* H-337: restore widget reply delivery * H-337: harden widget conversation ownership --------- Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
+22
-18
@@ -60,8 +60,8 @@ func NewWSAuthenticator(jwtService *auth.JWTService, contactInboxRepo *repositor
|
||||
// 1. Agent/User auth (primary): JWT token from 'token' query param or Authorization header.
|
||||
// Validates via jwtService.ValidateAccessToken, populates WSClaims from auth.Claims.
|
||||
//
|
||||
// 2. Contact auth (secondary): pubsub_token + user_id query params.
|
||||
// Looks up ContactInbox by pubsub_token, verifies the contact belongs to the account,
|
||||
// 2. Contact auth (secondary): pubsub_token query param.
|
||||
// Looks up ContactInbox by pubsub_token and resolves the contact and account,
|
||||
// populates WSClaims with contact identity.
|
||||
//
|
||||
// Returns WSClaims on success, or an error suitable for HTTP 401 rejection.
|
||||
@@ -98,18 +98,16 @@ func (a *WSAuthenticator) Authenticate(c *gin.Context) (*WSClaims, error) {
|
||||
// --- Path 2: Contact authentication via pubsub_token ---
|
||||
pubsubToken := c.Query("pubsub_token")
|
||||
if pubsubToken == "" {
|
||||
return nil, errors.New("authentication required: provide 'token' (JWT) or 'pubsub_token' + 'user_id' params")
|
||||
return nil, errors.New("authentication required: provide 'token' (JWT) or 'pubsub_token'")
|
||||
}
|
||||
|
||||
// Contact auth requires user_id param (Chatwoot RoomChannel: contact_id from params)
|
||||
userIDStr := c.Query("user_id")
|
||||
if userIDStr == "" {
|
||||
return nil, errors.New("contact auth requires 'user_id' parameter alongside 'pubsub_token'")
|
||||
}
|
||||
|
||||
contactID, err := strconv.ParseUint(userIDStr, 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid user_id parameter: %w", err)
|
||||
var providedContactID *uint
|
||||
if userIDStr := c.Query("user_id"); userIDStr != "" {
|
||||
contactID, parseErr := strconv.ParseUint(userIDStr, 10, 32)
|
||||
if parseErr != nil {
|
||||
return nil, fmt.Errorf("invalid user_id parameter: %w", parseErr)
|
||||
}
|
||||
value := uint(contactID)
|
||||
providedContactID = &value
|
||||
}
|
||||
|
||||
// Lookup ContactInbox by pubsub_token
|
||||
@@ -119,14 +117,17 @@ func (a *WSAuthenticator) Authenticate(c *gin.Context) (*WSClaims, error) {
|
||||
return nil, fmt.Errorf("invalid pubsub_token: %w", err)
|
||||
}
|
||||
|
||||
// Verify the contact ID matches
|
||||
if contactInbox.ContactID != uint(contactID) {
|
||||
logger.L().Debugf("ws auth: contact mismatch (expected=%d, found=%d)", uint(contactID), contactInbox.ContactID)
|
||||
return nil, errors.New("pubsub_token does not belong to the specified contact")
|
||||
// Legacy clients may still send user_id. Treat it as an additional
|
||||
// fail-closed check, while Chatwoot widgets authenticate by token alone.
|
||||
if providedContactID != nil {
|
||||
if contactInbox.ContactID != *providedContactID {
|
||||
logger.L().Debugf("ws auth: contact mismatch (expected=%d, found=%d)", *providedContactID, contactInbox.ContactID)
|
||||
return nil, errors.New("pubsub_token does not belong to the specified contact")
|
||||
}
|
||||
}
|
||||
|
||||
wsClaims := &WSClaims{
|
||||
UserID: uint(contactID), // for contacts, UserID maps to contact_id (Chatwoot convention)
|
||||
UserID: contactInbox.ContactID, // for contacts, UserID maps to contact_id (Chatwoot convention)
|
||||
AccountID: contactInbox.Contact.AccountID,
|
||||
Role: "contact",
|
||||
Provider: "pubsub_token",
|
||||
@@ -208,6 +209,9 @@ func (a *WSAuthenticator) AuthenticateAndServeWS(c *gin.Context) {
|
||||
// findContactInboxByPubsubToken looks up a ContactInbox by its PubsubToken field.
|
||||
// Preloads the associated Contact to resolve the AccountID for authorization.
|
||||
func (a *WSAuthenticator) findContactInboxByPubsubToken(ctx context.Context, pubsubToken string) (*model.ContactInbox, error) {
|
||||
if a.contactInboxRepo == nil {
|
||||
return nil, errors.New("contact inbox repository unavailable")
|
||||
}
|
||||
return a.contactInboxRepo.FindByPubsubToken(ctx, pubsubToken)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestWSAuthenticator_Authenticate_PubsubTokenNoUserID_Cov4(t *testing.T) {
|
||||
a := NewWSAuthenticator(nil, nil)
|
||||
_, err := a.Authenticate(c)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "user_id")
|
||||
assert.Contains(t, err.Error(), "invalid pubsub_token")
|
||||
}
|
||||
|
||||
func TestWSAuthenticator_Authenticate_PubsubTokenInvalidUserID_Cov4(t *testing.T) {
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestAuthenticate_PubsubTokenWithoutUserID_Cov5(t *testing.T) {
|
||||
claims, err := authenticator.Authenticate(c)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, claims)
|
||||
assert.Contains(t, err.Error(), "requires 'user_id' parameter")
|
||||
assert.Contains(t, err.Error(), "invalid pubsub_token")
|
||||
}
|
||||
|
||||
func TestAuthenticate_PubsubToken_InvalidUserID_Cov5(t *testing.T) {
|
||||
|
||||
@@ -142,7 +142,7 @@ func TestAuthenticate_PubsubNoUserID_Cov7(t *testing.T) {
|
||||
claims, err := a.Authenticate(c)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, claims)
|
||||
assert.Contains(t, err.Error(), "user_id")
|
||||
assert.Contains(t, err.Error(), "invalid pubsub_token")
|
||||
}
|
||||
|
||||
func TestAuthenticate_PubsubInvalidUserID_Cov7(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user