refactor: implement widget authentication middleware with standard HTTP headers

- Add widgetAuth middleware to handle JWT and inbox validation consistently
  - Move authentication logic from request body to standard HTTP headers:
    * JWT: Authorization: Bearer <token>
    * Inbox ID: X-Libredesk-Inbox-ID: <id>
  - Refactor all widget handlers to use middleware context instead of duplicate auth code
  - Frontend now sends auth headers via HTTP interceptor for all widget requests
This commit is contained in:
Abhinav Raut
2025-07-20 17:44:36 +05:30
parent e2bba04669
commit f05014f412
4 changed files with 281 additions and 198 deletions
+90 -177
View File
@@ -22,11 +22,6 @@ import (
"github.com/zerodha/fastglue"
)
type reqCommon struct {
JWT string `json:"jwt"`
InboxID int `json:"inbox_id"`
}
// Define JWT claims structure
type Claims struct {
UserID int `json:"user_id,omitempty"`
@@ -39,24 +34,11 @@ type Claims struct {
jwt.RegisteredClaims
}
// Chat widget initialization request
type chatInitReq struct {
reqCommon
VisitorName string `json:"visitor_name,omitempty"`
VisitorEmail string `json:"visitor_email,omitempty"`
Message string `json:"message,omitempty"`
}
type conversationResp struct {
Conversation cmodels.ChatConversation `json:"conversation"`
Messages []cmodels.ChatMessage `json:"messages"`
}
type chatMessageReq struct {
Message string `json:"message"`
reqCommon
}
type chatSettingsResponse struct {
livechat.Config
BusinessHours []bhmodels.BusinessHours `json:"business_hours,omitempty"`
@@ -175,7 +157,11 @@ func handleGetChatSettings(r *fastglue.Request) error {
func handleChatInit(r *fastglue.Request) error {
var (
app = r.Context.(*App)
req = chatInitReq{}
req = struct {
VisitorName string `json:"visitor_name,omitempty"`
VisitorEmail string `json:"visitor_email,omitempty"`
Message string `json:"message,omitempty"`
}{}
)
if err := r.Decode(&req, "json"); err != nil {
@@ -187,17 +173,20 @@ func handleChatInit(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.message}"), nil, envelope.InputError)
}
// Make sure the inbox is enabled and of the correct type.
inbox, err := app.inbox.GetDBRecord(req.InboxID)
// Get authenticated data from context (set by middleware)
// Middleware always validates inbox, so we can safely use non-optional getters
claims := getWidgetClaimsOptional(r)
inboxID, err := getWidgetInboxID(r)
if err != nil {
app.lo.Error("error fetching inbox", "inbox_id", req.InboxID, "error", err)
return sendErrorEnvelope(r, err)
app.lo.Error("error getting inbox ID from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
}
if !inbox.Enabled {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.disabled", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
}
if inbox.Channel != livechat.ChannelLiveChat {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
inbox, err := getWidgetInbox(r)
if err != nil {
app.lo.Error("error getting inbox from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
}
var (
@@ -205,6 +194,7 @@ func handleChatInit(r *fastglue.Request) error {
conversationUUID string
isVisitor bool
config livechat.Config
newJWT string
)
// Parse inbox config
@@ -213,18 +203,16 @@ func handleChatInit(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.invalid", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
}
// Handle authenticated user.
if req.JWT != "" {
var claims Claims
var err error
claims, err = verifyStandardJWT(req.JWT, inbox.Secret.String)
// Handle authenticated user vs visitor
if claims != nil {
// Use authenticated contact ID from middleware
contactID, err = getWidgetContactID(r)
if err != nil {
app.lo.Error("invalid inbox JWT", "jwt", req.JWT, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.messages.sessionExpired"), nil, envelope.UnauthorizedError)
app.lo.Error("error getting contact ID from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
}
// Handle existing contacts they all external user id.
// Handle existing contacts with external user id - check if we need to create user
if claims.ExternalUserID != "" {
// Find or create user based on external_user_id.
user, err := app.user.GetByExternalID(claims.ExternalUserID)
@@ -262,7 +250,6 @@ func handleChatInit(r *fastglue.Request) error {
contactID = user.ID
isVisitor = false
} else {
contactID = claims.UserID
isVisitor = claims.IsVisitor
}
} else {
@@ -279,7 +266,7 @@ func handleChatInit(r *fastglue.Request) error {
}
contactID = visitor.ID
secretToUse := []byte(inbox.Secret.String)
req.JWT, err = generateUserJWTWithSecret(contactID, isVisitor, time.Now().Add(87600*time.Hour), secretToUse) // 10 years
newJWT, err = generateUserJWTWithSecret(contactID, isVisitor, time.Now().Add(87600*time.Hour), secretToUse) // 10 years
if err != nil {
app.lo.Error("error generating visitor JWT", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorGenerating", "name", "{globals.terms.session}"), nil, envelope.GeneralError)
@@ -297,7 +284,7 @@ func handleChatInit(r *fastglue.Request) error {
}
if userConfig.PreventMultipleConversations {
conversations, err := app.conversation.GetContactChatConversations(contactID, req.InboxID)
conversations, err := app.conversation.GetContactChatConversations(contactID, inboxID)
if err != nil {
userType := "visitor"
if !isVisitor {
@@ -316,12 +303,12 @@ func handleChatInit(r *fastglue.Request) error {
}
}
app.lo.Info("creating new live chat conversation for user", "user_id", contactID, "inbox_id", req.InboxID, "is_visitor", isVisitor)
app.lo.Info("creating new live chat conversation for user", "user_id", contactID, "inbox_id", inboxID, "is_visitor", isVisitor)
// Create conversation.
_, conversationUUID, err = app.conversation.CreateConversation(
contactID,
req.InboxID,
inboxID,
"",
time.Now(),
"",
@@ -370,13 +357,20 @@ func handleChatInit(r *fastglue.Request) error {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(map[string]any{
// For visitors, return the new JWT. For authenticated users, no JWT is needed in response.
response := map[string]any{
"conversation": resp.Conversation,
"messages": resp.Messages,
"business_hours_id": resp.BusinessHoursID,
"working_hours_utc_offset": resp.WorkingHoursUTCOffset,
"jwt": req.JWT,
})
}
// Only add JWT for visitor creation
if newJWT != "" {
response["jwt"] = newJWT
}
return r.SendEnvelope(response)
}
// handleChatUpdateLastSeen updates contact last seen timestamp for a conversation
@@ -384,16 +378,17 @@ func handleChatUpdateLastSeen(r *fastglue.Request) error {
var (
app = r.Context.(*App)
conversationUUID = r.RequestCtx.UserValue("uuid").(string)
req = reqCommon{}
)
if conversationUUID == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.conversation}"), nil, envelope.InputError)
}
if err := r.Decode(&req, "json"); err != nil {
app.lo.Error("error unmarshalling chat update last seen request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
// Get authenticated data from middleware context
contactID, err := getWidgetContactID(r)
if err != nil {
app.lo.Error("error getting contact ID from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
}
conversation, err := app.conversation.GetConversation(0, conversationUUID)
@@ -402,26 +397,6 @@ func handleChatUpdateLastSeen(r *fastglue.Request) error {
return sendErrorEnvelope(r, err)
}
inbox, err := app.inbox.GetDBRecord(conversation.InboxID)
if err != nil {
app.lo.Error("error fetching inbox", "inbox_id", conversation.InboxID, "error", err)
return sendErrorEnvelope(r, err)
}
// Verify JWT.
claims, err := verifyStandardJWT(req.JWT, inbox.Secret.String)
if err != nil {
app.lo.Error("invalid JWT", "jwt", req.JWT, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
contactID, err := resolveUserIDFromClaims(app, claims)
if err != nil {
app.lo.Error("error resolving user ID from JWT claims", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Make sure the conversation belongs to the contact.
if conversation.ContactID != contactID {
app.lo.Error("unauthorized access to conversation", "conversation_uuid", conversationUUID, "contact_id", contactID, "conversation_contact_id", conversation.ContactID)
@@ -442,47 +417,26 @@ func handleChatGetConversation(r *fastglue.Request) error {
var (
app = r.Context.(*App)
conversationUUID = r.RequestCtx.UserValue("uuid").(string)
chatReq = chatInitReq{}
)
if conversationUUID == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "conversation_id is required", nil, envelope.InputError)
}
// Decode chat request if present
if err := r.Decode(&chatReq, "json"); err != nil {
app.lo.Error("error unmarshalling chat request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
// Get authenticated data from middleware context
contactID, err := getWidgetContactID(r)
if err != nil {
app.lo.Error("error getting contact ID from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
}
// Fetch conversation first to get inbox info
// Fetch conversation
conversation, err := app.conversation.GetConversation(0, conversationUUID)
if err != nil {
app.lo.Error("error fetching conversation", "conversation_uuid", conversationUUID, "error", err)
return sendErrorEnvelope(r, err)
}
// Get inbox to retrieve secret for JWT verification
inbox, err := app.inbox.GetDBRecord(conversation.InboxID)
if err != nil {
app.lo.Error("error fetching inbox", "inbox_id", conversation.InboxID, "error", err)
return sendErrorEnvelope(r, err)
}
// Verify JWT.
claims, err := verifyStandardJWT(chatReq.JWT, inbox.Secret.String)
if err != nil {
app.lo.Error("invalid JWT", "jwt", chatReq.JWT, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
contactID, err := resolveUserIDFromClaims(app, claims)
if err != nil {
app.lo.Error("error resolving user ID from JWT claims", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Make sure the conversation belongs to the contact.
if conversation.ContactID != contactID {
app.lo.Error("unauthorized access to conversation", "conversation_uuid", conversationUUID, "contact_id", contactID, "conversation_contact_id", conversation.ContactID)
@@ -502,37 +456,23 @@ func handleChatGetConversation(r *fastglue.Request) error {
func handleGetConversations(r *fastglue.Request) error {
var (
app = r.Context.(*App)
req = reqCommon{}
)
if err := r.Decode(&req, "json"); err != nil {
app.lo.Error("error unmarshalling chat conversations request", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.InputError)
// Get authenticated data from middleware context
contactID, err := getWidgetContactID(r)
if err != nil {
app.lo.Error("error getting contact ID from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
}
// Get inbox to retrieve secret for JWT verification
inbox, err := app.inbox.GetDBRecord(req.InboxID)
inboxID, err := getWidgetInboxID(r)
if err != nil {
app.lo.Error("error fetching inbox", "inbox_id", req.InboxID, "error", err)
return sendErrorEnvelope(r, err)
}
// Verify JWT.
claims, err := verifyStandardJWT(req.JWT, inbox.Secret.String)
if err != nil {
app.lo.Error("invalid JWT", "jwt", req.JWT, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
contactID, err := resolveUserIDFromClaims(app, claims)
if err != nil {
app.lo.Error("error resolving user ID from JWT claims", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
app.lo.Error("error getting inbox ID from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
}
// Fetch conversations for the contact and convert to ChatConversation format.
chatConversations, err := app.conversation.GetContactChatConversations(contactID, req.InboxID)
chatConversations, err := app.conversation.GetContactChatConversations(contactID, inboxID)
if err != nil {
app.lo.Error("error fetching conversations for contact", "contact_id", contactID, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.conversation}"), nil, envelope.GeneralError)
@@ -546,9 +486,10 @@ func handleChatSendMessage(r *fastglue.Request) error {
var (
app = r.Context.(*App)
conversationUUID = r.RequestCtx.UserValue("uuid").(string)
req = chatMessageReq{}
senderType = cmodels.SenderTypeContact
senderID = 0
req = struct {
Message string `json:"message"`
}{}
senderType = cmodels.SenderTypeContact
)
if err := r.Decode(&req, "json"); err != nil {
@@ -560,33 +501,26 @@ func handleChatSendMessage(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.message}"), nil, envelope.InputError)
}
// Fetch conversation to ensure it exists and get inbox info.
// Get authenticated data from middleware context
senderID, err := getWidgetContactID(r)
if err != nil {
app.lo.Error("error getting contact ID from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
}
inbox, err := getWidgetInbox(r)
if err != nil {
app.lo.Error("error getting inbox from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
}
// Fetch conversation to ensure it exists
conversation, err := app.conversation.GetConversation(0, conversationUUID)
if err != nil {
app.lo.Error("error fetching conversation", "conversation_uuid", conversationUUID, "error", err)
return sendErrorEnvelope(r, err)
}
// Get inbox to retrieve secret for JWT verification
inbox, err := app.inbox.GetDBRecord(conversation.InboxID)
if err != nil {
app.lo.Error("error fetching inbox", "inbox_id", conversation.InboxID, "error", err)
return sendErrorEnvelope(r, err)
}
claims, err := verifyStandardJWT(req.JWT, inbox.Secret.String)
if err != nil {
app.lo.Error("invalid JWT", "jwt", req.JWT, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
senderID, err = resolveUserIDFromClaims(app, claims)
if err != nil {
app.lo.Error("error resolving user ID from JWT claims", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Fetch sender.
sender, err := app.user.Get(senderID, "", "")
if err != nil {
@@ -655,8 +589,7 @@ func handleChatSendMessage(r *fastglue.Request) error {
// handleWidgetMediaUpload handles media uploads for the widget.
func handleWidgetMediaUpload(r *fastglue.Request) error {
var (
app = r.Context.(*App)
senderID = 0
app = r.Context.(*App)
)
form, err := r.RequestCtx.MultipartForm()
@@ -665,12 +598,18 @@ func handleWidgetMediaUpload(r *fastglue.Request) error {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorParsing", "name", "{globals.terms.request}"), nil, envelope.GeneralError)
}
// Get JWT token from form data
jwtValues, jwtOk := form.Value["jwt"]
if !jwtOk || len(jwtValues) == 0 || jwtValues[0] == "" {
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
// Get authenticated data from middleware context
senderID, err := getWidgetContactID(r)
if err != nil {
app.lo.Error("error getting contact ID from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.user}"), nil, envelope.GeneralError)
}
inbox, err := getWidgetInbox(r)
if err != nil {
app.lo.Error("error getting inbox from middleware context", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.inbox}"), nil, envelope.GeneralError)
}
jwtToken := jwtValues[0]
// Get conversation UUID from form data
conversationValues, convOk := form.Value["conversation_uuid"]
@@ -686,32 +625,6 @@ func handleWidgetMediaUpload(r *fastglue.Request) error {
return sendErrorEnvelope(r, err)
}
// Get inbox to retrieve secret for JWT verification
inbox, err := app.inbox.GetDBRecord(conversation.InboxID)
if err != nil {
app.lo.Error("error fetching inbox", "inbox_id", conversation.InboxID, "error", err)
return sendErrorEnvelope(r, err)
}
// Make sure the inbox is enabled.
if !inbox.Enabled {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.disabled", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
}
// Verify JWT and get user information
claims, err := verifyStandardJWT(jwtToken, inbox.Secret.String)
if err != nil {
app.lo.Error("invalid JWT", "jwt", jwtToken, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Resolve user ID from JWT claims (handles both user_id and external_user_id)
senderID, err = resolveUserIDFromClaims(app, claims)
if err != nil {
app.lo.Error("error resolving user ID from JWT claims", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
if conversation.ContactID != senderID {
app.lo.Error("access denied: user attempted to access conversation owned by different contact", "conversation_uuid", conversationUUID, "requesting_contact_id", senderID, "conversation_owner_id", conversation.ContactID)
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.Ts("globals.messages.denied", "name", "{globals.terms.permission}"), nil, envelope.PermissionError)
@@ -1060,4 +973,4 @@ func generateUserJWTWithSecret(userID int, isVisitor bool, expirationTime time.T
return "", err
}
return tokenString, nil
}
}
+6 -6
View File
@@ -227,12 +227,12 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
// Widget APIs.
g.GET("/api/v1/widget/chat/settings/launcher", handleGetChatLauncherSettings)
g.GET("/api/v1/widget/chat/settings", handleGetChatSettings)
g.POST("/api/v1/widget/chat/conversations/init", handleChatInit)
g.POST("/api/v1/widget/chat/conversations", handleGetConversations)
g.POST("/api/v1/widget/chat/conversations/{uuid}/update-last-seen", handleChatUpdateLastSeen)
g.POST("/api/v1/widget/chat/conversations/{uuid}", handleChatGetConversation)
g.POST("/api/v1/widget/chat/conversations/{uuid}/message", handleChatSendMessage)
g.POST("/api/v1/widget/media/upload", handleWidgetMediaUpload)
g.POST("/api/v1/widget/chat/conversations/init", widgetAuth(handleChatInit))
g.POST("/api/v1/widget/chat/conversations", widgetAuth(handleGetConversations))
g.POST("/api/v1/widget/chat/conversations/{uuid}/update-last-seen", widgetAuth(handleChatUpdateLastSeen))
g.POST("/api/v1/widget/chat/conversations/{uuid}", widgetAuth(handleChatGetConversation))
g.POST("/api/v1/widget/chat/conversations/{uuid}/message", widgetAuth(handleChatSendMessage))
g.POST("/api/v1/widget/media/upload", widgetAuth(handleWidgetMediaUpload))
// Frontend pages.
g.GET("/", notAuthPage(serveIndexPage))
+153
View File
@@ -0,0 +1,153 @@
package main
import (
"fmt"
"strconv"
"strings"
"github.com/abhinavxd/libredesk/internal/envelope"
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat"
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastglue"
)
const (
// Context keys for storing authenticated widget data
ctxWidgetClaims = "widget_claims"
ctxWidgetInboxID = "widget_inbox_id"
ctxWidgetContactID = "widget_contact_id"
ctxWidgetInbox = "widget_inbox"
// Header sent in every widget request to identify the inbox
hdrWidgetInboxID = "X-Libredesk-Inbox-ID"
)
// widgetAuth middleware authenticates widget requests using JWT and inbox validation.
// It always validates the inbox from X-Libredesk-Inbox-ID header, and conditionally validates JWT.
// For /conversations/init without JWT, it allows visitor creation while still validating inbox.
func widgetAuth(next func(*fastglue.Request) error) func(*fastglue.Request) error {
return func(r *fastglue.Request) error {
var (
app = r.Context.(*App)
)
// Always extract and validate inbox_id from custom header
inboxIDHeader := string(r.RequestCtx.Request.Header.Peek(hdrWidgetInboxID))
if inboxIDHeader == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
}
inboxID, err := strconv.Atoi(inboxIDHeader)
if err != nil || inboxID <= 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
}
// Always fetch and validate inbox
inbox, err := app.inbox.GetDBRecord(inboxID)
if err != nil {
app.lo.Error("error fetching inbox", "inbox_id", inboxID, "error", err)
return sendErrorEnvelope(r, err)
}
if !inbox.Enabled {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.disabled", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
}
// Check if inbox is the correct type for widget requests
if inbox.Channel != livechat.ChannelLiveChat {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
}
// Always store inbox data in context
r.RequestCtx.SetUserValue(ctxWidgetInboxID, inboxID)
r.RequestCtx.SetUserValue(ctxWidgetInbox, inbox)
// Extract JWT from Authorization header (Bearer token)
authHeader := string(r.RequestCtx.Request.Header.Peek("Authorization"))
// For init endpoint, allow requests without JWT (visitor creation)
if authHeader == "" && strings.Contains(string(r.RequestCtx.Path()), "/conversations/init") {
return next(r)
}
// For all other requests, require JWT
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
jwtToken := strings.TrimPrefix(authHeader, "Bearer ")
// Verify JWT using inbox secret
claims, err := verifyStandardJWT(jwtToken, inbox.Secret.String)
if err != nil {
app.lo.Error("invalid JWT", "jwt", jwtToken, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Resolve user/contact ID from JWT claims
contactID, err := resolveUserIDFromClaims(app, claims)
if err != nil {
app.lo.Error("error resolving user ID from JWT claims", "error", err)
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
}
// Store authenticated data in request context for downstream handlers
r.RequestCtx.SetUserValue(ctxWidgetClaims, claims)
r.RequestCtx.SetUserValue(ctxWidgetContactID, contactID)
return next(r)
}
}
// Helper functions to extract authenticated data from request context
// getWidgetInboxID extracts inbox ID from request context
func getWidgetInboxID(r *fastglue.Request) (int, error) {
val := r.RequestCtx.UserValue(ctxWidgetInboxID)
if val == nil {
return 0, fmt.Errorf("widget middleware not applied: missing inbox ID in context")
}
inboxID, ok := val.(int)
if !ok {
return 0, fmt.Errorf("invalid inbox ID type in context")
}
return inboxID, nil
}
// getWidgetContactID extracts contact ID from request context
func getWidgetContactID(r *fastglue.Request) (int, error) {
val := r.RequestCtx.UserValue(ctxWidgetContactID)
if val == nil {
return 0, fmt.Errorf("widget middleware not applied: missing contact ID in context")
}
contactID, ok := val.(int)
if !ok {
return 0, fmt.Errorf("invalid contact ID type in context")
}
return contactID, nil
}
// getWidgetInbox extracts inbox model from request context
func getWidgetInbox(r *fastglue.Request) (imodels.Inbox, error) {
val := r.RequestCtx.UserValue(ctxWidgetInbox)
if val == nil {
return imodels.Inbox{}, fmt.Errorf("widget middleware not applied: missing inbox in context")
}
inbox, ok := val.(imodels.Inbox)
if !ok {
return imodels.Inbox{}, fmt.Errorf("invalid inbox type in context")
}
return inbox, nil
}
// getWidgetClaimsOptional extracts JWT claims from request context, returns nil if not set
func getWidgetClaimsOptional(r *fastglue.Request) *Claims {
val := r.RequestCtx.UserValue(ctxWidgetClaims)
if val == nil {
return nil
}
if claims, ok := val.(Claims); ok {
return &claims
}
return nil
}
+32 -15
View File
@@ -11,19 +11,25 @@ const http = axios.create({
responseType: 'json'
})
// Set content type if not specified and add libredesk_session to POST/PUT requests
// Set content type and authentication headers
http.interceptors.request.use((request) => {
if ((request.method === 'post' || request.method === 'put') && !request.headers['Content-Type']) {
request.headers['Content-Type'] = 'application/json'
}
// Add libredesk_session to POST/PUT request data
if (request.method === 'post' || request.method === 'put') {
// Add authentication headers for widget API endpoints
if (request.url && request.url.includes('/api/v1/widget/')) {
const libredeskSession = localStorage.getItem('libredesk_session')
request.data = {
...request.data,
inbox_id: getInboxIDFromQuery(),
jwt: libredeskSession
const inboxId = getInboxIDFromQuery()
// Add JWT to Authorization header
if (libredeskSession) {
request.headers['Authorization'] = `Bearer ${libredeskSession}`
}
// Add inbox ID to custom header
if (inboxId) {
request.headers['X-Libredesk-Inbox-ID'] = inboxId.toString()
}
}
@@ -41,22 +47,33 @@ const sendChatMessage = (uuid, data) => http.post(`/api/v1/widget/chat/conversat
const closeChatConversation = (uuid) => http.post(`/api/v1/widget/chat/conversations/${uuid}/close`)
const uploadMedia = (conversationUUID, files) => {
const formData = new FormData()
// Add JWT token
const libredeskSession = localStorage.getItem('libredesk_session')
formData.append('jwt', libredeskSession)
// Only add conversation UUID to form data now
formData.append('conversation_uuid', conversationUUID)
formData.append('inbox_id', getInboxIDFromQuery())
// Add files
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i])
}
// Get authentication data for headers
const libredeskSession = localStorage.getItem('libredesk_session')
const inboxId = getInboxIDFromQuery()
const headers = {
'Content-Type': 'multipart/form-data'
}
// Add authentication headers
if (libredeskSession) {
headers['Authorization'] = `Bearer ${libredeskSession}`
}
if (inboxId) {
headers['X-Libredesk-Inbox-ID'] = inboxId.toString()
}
return axios.post('/api/v1/widget/media/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
headers,
timeout: 30000
})
}