* fix(security): harden auth and credential handling (HH-444) * fix(security): address HH-444 review blockers * fix(security): close remaining HH-444 review blockers --------- Co-authored-by: Rogee <rogee@ipao.vip>
572 lines
19 KiB
Go
572 lines
19 KiB
Go
package v1
|
|
|
|
import (
|
|
crypto_rand "crypto/rand"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gochat/gochat/internal/auth"
|
|
"github.com/gochat/gochat/internal/service"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// Reference: P2E §1 + P2C §Auth routes
|
|
// Auth HTTP handlers are thin wrappers around auth_service business logic.
|
|
// Maps to Chatwoot auth controllers:
|
|
// - auth/sessions_controller → Login
|
|
// - auth/registrations_controller → Register
|
|
// - auth/token_refreshes_controller → Refresh
|
|
// - auth/sessions_controller#destroy → Logout
|
|
// - accounts_controller#switch → SwitchAccount
|
|
// - auth/passwords_controller → ResetPassword
|
|
// - auth/confirmations_controller → ConfirmEmail
|
|
// - omniauth_callbacks_controller → OAuthCallback
|
|
|
|
// AuthHandler handles authentication HTTP endpoints.
|
|
type AuthHandler struct {
|
|
authService *service.AuthService
|
|
profileService *service.ProfileService
|
|
wsTickets *auth.WSTicketStore
|
|
secureCookies bool
|
|
}
|
|
|
|
const (
|
|
browserRefreshCookie = "_gochat_refresh"
|
|
browserSessionMarker = "cw_d_session_state"
|
|
)
|
|
|
|
func (h *AuthHandler) WithWSTicketStore(store *auth.WSTicketStore) *AuthHandler {
|
|
h.wsTickets = store
|
|
return h
|
|
}
|
|
|
|
func (h *AuthHandler) WithSecureCookies(secure bool) *AuthHandler {
|
|
h.secureCookies = secure
|
|
return h
|
|
}
|
|
|
|
// NewAuthHandler creates an auth handler with service dependencies.
|
|
func NewAuthHandler(authService *service.AuthService, profileService ...*service.ProfileService) *AuthHandler {
|
|
var profileSvc *service.ProfileService
|
|
if len(profileService) > 0 {
|
|
profileSvc = profileService[0]
|
|
}
|
|
return &AuthHandler{
|
|
authService: authService,
|
|
profileService: profileSvc,
|
|
}
|
|
}
|
|
|
|
// --- Request/Response structs ---
|
|
|
|
// LoginRequest is the JSON body for login endpoint.
|
|
type LoginRequest struct {
|
|
Email string `json:"email" binding:"required"`
|
|
Password string `json:"password" binding:"required,min=6"`
|
|
}
|
|
|
|
// RefreshRequest is the JSON body for refresh endpoint.
|
|
type RefreshRequest struct {
|
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
|
}
|
|
|
|
// SwitchAccountRequest is the JSON body for account switching.
|
|
type SwitchAccountRequest struct {
|
|
AccountID uint `json:"account_id" binding:"required"`
|
|
}
|
|
|
|
// ResetPasswordRequest is the JSON body for password reset.
|
|
type ResetPasswordRequest struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
}
|
|
|
|
type ConfirmResetPasswordRequest struct {
|
|
ResetPasswordToken string `json:"reset_password_token" binding:"required"`
|
|
Password string `json:"password" binding:"required,min=6"`
|
|
PasswordConfirmation string `json:"password_confirmation" binding:"required,min=6"`
|
|
}
|
|
|
|
type ConfirmEmailRequest struct {
|
|
ConfirmationToken string `json:"confirmation_token" binding:"required"`
|
|
}
|
|
|
|
// --- Handlers ---
|
|
|
|
// Login authenticates a user with email/password and returns JWT tokens.
|
|
// POST /api/v1/auth/login
|
|
func (h *AuthHandler) Login(c *gin.Context) {
|
|
var req LoginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
output, err := h.authService.Login(c.Request.Context(), &service.LoginInput{
|
|
Email: req.Email,
|
|
Password: req.Password,
|
|
})
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, err.Error())
|
|
return
|
|
}
|
|
if err := h.trackChatwootSession(c, output); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create session")
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"user": output.User,
|
|
"access_token": output.TokenPair.AccessToken,
|
|
"refresh_token": output.TokenPair.RefreshToken,
|
|
"expires_at": output.TokenPair.ExpiresAt,
|
|
"account_id": output.AccountID,
|
|
"role": output.Role,
|
|
})
|
|
}
|
|
|
|
// ChatwootSignIn authenticates through the DeviseTokenAuth-compatible route.
|
|
// POST /auth/sign_in
|
|
func (h *AuthHandler) ChatwootSignIn(c *gin.Context) {
|
|
var req LoginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
output, err := h.authService.Login(c.Request.Context(), &service.LoginInput{
|
|
Email: req.Email,
|
|
Password: req.Password,
|
|
})
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.trackChatwootSession(c, output); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create session")
|
|
return
|
|
}
|
|
|
|
h.setBrowserSession(c, output)
|
|
h.setChatwootAuthHeaders(c, output)
|
|
profile, err := h.chatwootUserPayload(c, output.User.ID, output.AccountID)
|
|
if err != nil {
|
|
handleServiceError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": profile})
|
|
}
|
|
|
|
// ChatwootValidateToken returns the current user payload for Auth.validityCheck.
|
|
// GET /auth/validate_token
|
|
func (h *AuthHandler) ChatwootValidateToken(c *gin.Context) {
|
|
accessToken := extractChatwootAccessToken(c)
|
|
var output *service.LoginOutput
|
|
var err error
|
|
if accessToken != "" {
|
|
output, err = h.authService.ValidateAccessToken(c.Request.Context(), accessToken)
|
|
}
|
|
if err != nil {
|
|
accessToken = ""
|
|
}
|
|
if accessToken == "" {
|
|
refreshToken, cookieErr := c.Cookie(browserRefreshCookie)
|
|
if cookieErr != nil {
|
|
h.clearBrowserSession(c)
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "invalid session")
|
|
return
|
|
}
|
|
refreshed, refreshErr := h.authService.Refresh(c.Request.Context(), &service.RefreshInput{RefreshToken: refreshToken})
|
|
if refreshErr != nil {
|
|
h.clearBrowserSession(c)
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "invalid session")
|
|
return
|
|
}
|
|
output = &service.LoginOutput{
|
|
User: refreshed.User, TokenPair: refreshed.TokenPair, AccountID: refreshed.AccountID,
|
|
Role: refreshed.Role, ClientID: refreshed.ClientID,
|
|
}
|
|
accessToken = refreshed.TokenPair.AccessToken
|
|
h.setBrowserSession(c, output)
|
|
}
|
|
|
|
h.setChatwootAuthHeaders(c, output)
|
|
c.Header("access-token", accessToken)
|
|
profile, err := h.chatwootUserPayload(c, output.User.ID, output.AccountID)
|
|
if err != nil {
|
|
handleServiceError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"payload": gin.H{
|
|
"success": true,
|
|
"data": profile,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ChatwootSignOut revokes the current session for the DeviseTokenAuth route.
|
|
// DELETE /auth/sign_out
|
|
func (h *AuthHandler) ChatwootSignOut(c *gin.Context) {
|
|
h.clearBrowserSession(c)
|
|
accessToken := extractChatwootAccessToken(c)
|
|
authenticated := false
|
|
var revokeErr error
|
|
if accessToken != "" {
|
|
if output, validateErr := h.authService.ValidateAccessToken(c.Request.Context(), accessToken); validateErr == nil {
|
|
authenticated = true
|
|
revokeErr = h.authService.RevokeChatwootSession(c.Request.Context(), output.User.ID, output.ClientID)
|
|
}
|
|
}
|
|
if refreshToken, cookieErr := c.Cookie(browserRefreshCookie); cookieErr == nil {
|
|
if err := h.authService.RevokeBrowserSession(c.Request.Context(), refreshToken); err == nil {
|
|
authenticated = true
|
|
} else if !authenticated {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "invalid session")
|
|
return
|
|
}
|
|
}
|
|
if !authenticated {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "authentication required")
|
|
return
|
|
}
|
|
if revokeErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Logout failed")
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
// Refresh rotates a refresh token and returns new JWT pair.
|
|
// POST /api/v1/auth/refresh
|
|
// Implements refresh token rotation per P2E §1.4 security requirement.
|
|
func (h *AuthHandler) Refresh(c *gin.Context) {
|
|
var req RefreshRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
output, err := h.authService.Refresh(c.Request.Context(), &service.RefreshInput{
|
|
RefreshToken: req.RefreshToken,
|
|
})
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, err.Error())
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"user": output.User,
|
|
"access_token": output.TokenPair.AccessToken,
|
|
"refresh_token": output.TokenPair.RefreshToken,
|
|
"expires_at": output.TokenPair.ExpiresAt,
|
|
})
|
|
}
|
|
|
|
// Logout revokes a user's refresh token.
|
|
// DELETE /api/v1/auth/logout
|
|
// Requires authentication — uses user_id from JWT context.
|
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
|
token := extractChatwootAccessToken(c)
|
|
if token == "" || h.authService == nil {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
output, err := h.authService.ValidateAccessToken(c.Request.Context(), token)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
|
|
if err := h.authService.RevokeChatwootSession(c.Request.Context(), output.User.ID, output.ClientID); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Logout failed")
|
|
return
|
|
}
|
|
|
|
response.NoContent(c)
|
|
}
|
|
|
|
// IssueWSTicket returns a short-lived, one-time credential for WebSocket upgrade.
|
|
func (h *AuthHandler) IssueWSTicket(c *gin.Context) {
|
|
if h.wsTickets == nil || h.authService == nil {
|
|
response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrInternal, "websocket authentication unavailable")
|
|
return
|
|
}
|
|
token := extractChatwootAccessToken(c)
|
|
if token == "" {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "invalid token")
|
|
return
|
|
}
|
|
output, err := h.authService.ValidateAccessToken(c.Request.Context(), token)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "invalid token")
|
|
return
|
|
}
|
|
ticket, err := h.wsTickets.Issue(c.Request.Context(), auth.WSTicketClaims{
|
|
UserID: output.User.ID, AccountID: output.AccountID, Role: output.Role, Provider: output.User.Provider, ClientID: output.ClientID,
|
|
})
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrInternal, "websocket authentication unavailable")
|
|
return
|
|
}
|
|
response.OK(c, gin.H{"ticket": ticket})
|
|
}
|
|
|
|
// SwitchAccount generates new tokens with a different account scope.
|
|
// POST /api/v1/auth/switch_account
|
|
// Requires authentication — uses user_id from JWT context.
|
|
func (h *AuthHandler) SwitchAccount(c *gin.Context) {
|
|
token := extractChatwootAccessToken(c)
|
|
if _, hasLegacyUserContext := c.Get("user_id"); token == "" && !hasLegacyUserContext {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
|
|
var req SwitchAccountRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
if token == "" || h.authService == nil {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
login, err := h.authService.ValidateAccessToken(c.Request.Context(), token)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
|
|
output, err := h.authService.SwitchAccount(c.Request.Context(), &service.SwitchAccountInput{
|
|
UserID: login.User.ID,
|
|
AccountID: req.AccountID,
|
|
ClientID: login.ClientID,
|
|
})
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, err.Error())
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"access_token": output.TokenPair.AccessToken,
|
|
"refresh_token": output.TokenPair.RefreshToken,
|
|
"expires_at": output.TokenPair.ExpiresAt,
|
|
"account_id": output.AccountID,
|
|
"role": output.Role,
|
|
})
|
|
}
|
|
|
|
// ResetPassword initiates password reset by sending reset email.
|
|
// POST /api/v1/auth/reset_password
|
|
func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
|
var req ResetPasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
// Always return success even if email doesn't exist (security best practice)
|
|
if err := h.authService.ResetPassword(c.Request.Context(), &service.ResetPasswordInput{
|
|
Email: req.Email,
|
|
}); err != nil {
|
|
applogger.L().Warnf("reset password request failed: %v", err)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": service.ChatwootPasswordResetMessage})
|
|
}
|
|
|
|
// ConfirmResetPassword completes Chatwoot's Devise-compatible password reset.
|
|
// PUT /auth/password
|
|
func (h *AuthHandler) ConfirmResetPassword(c *gin.Context) {
|
|
var req ConfirmResetPasswordRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
output, err := h.authService.ConfirmResetPassword(c.Request.Context(), &service.ConfirmResetPasswordInput{
|
|
Token: req.ResetPasswordToken,
|
|
Password: req.Password,
|
|
PasswordConfirmation: req.PasswordConfirmation,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"message": err.Error(), "redirect_url": "/"})
|
|
return
|
|
}
|
|
if err := h.trackChatwootSession(c, output); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create session")
|
|
return
|
|
}
|
|
|
|
h.setBrowserSession(c, output)
|
|
h.setChatwootAuthHeaders(c, output)
|
|
data, err := h.chatwootUserPayload(c, output.User.ID, output.AccountID)
|
|
if err != nil {
|
|
handleServiceError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": data})
|
|
}
|
|
|
|
// ConfirmEmail verifies email confirmation token.
|
|
// GET /api/v1/auth/confirm_email?token=xxx
|
|
func (h *AuthHandler) ConfirmEmail(c *gin.Context) {
|
|
token := c.Query("token")
|
|
if token == "" {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "Confirmation token required")
|
|
return
|
|
}
|
|
|
|
output, err := h.authService.ConfirmEmail(c.Request.Context(), &service.ConfirmEmailInput{
|
|
Token: token,
|
|
})
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"user": output.User,
|
|
"message": "Email confirmed successfully.",
|
|
})
|
|
}
|
|
|
|
// ChatwootConfirmEmail verifies the confirmation token from the reused frontend.
|
|
// POST /auth/confirmation
|
|
func (h *AuthHandler) ChatwootConfirmEmail(c *gin.Context) {
|
|
var req ConfirmEmailRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
output, err := h.authService.ConfirmEmail(c.Request.Context(), &service.ConfirmEmailInput{Token: req.ConfirmationToken})
|
|
if err != nil {
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"message": err.Error(), "redirect_url": "/"})
|
|
return
|
|
}
|
|
if err := h.trackChatwootSession(c, output); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create session")
|
|
return
|
|
}
|
|
|
|
h.setBrowserSession(c, output)
|
|
h.setChatwootAuthHeaders(c, output)
|
|
data, err := h.chatwootUserPayload(c, output.User.ID, output.AccountID)
|
|
if err != nil {
|
|
handleServiceError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": data})
|
|
}
|
|
|
|
// RegisterAuthRoutes sets up auth routes on a Gin router group.
|
|
// These routes are PUBLIC — no AuthRequired middleware.
|
|
func RegisterAuthRoutes(rg *gin.RouterGroup, handler *AuthHandler) {
|
|
authGroup := rg.Group("/auth")
|
|
{
|
|
// Core auth endpoints
|
|
authGroup.POST("/login", handler.Login)
|
|
authGroup.POST("/refresh", handler.Refresh)
|
|
authGroup.POST("/ws_ticket", handler.IssueWSTicket)
|
|
authGroup.DELETE("/logout", handler.Logout)
|
|
|
|
// Account management
|
|
authGroup.POST("/switch_account", handler.SwitchAccount)
|
|
|
|
// Password & email
|
|
authGroup.POST("/reset_password", handler.ResetPassword)
|
|
authGroup.PUT("/reset_password", handler.ConfirmResetPassword)
|
|
authGroup.GET("/confirm_email", handler.ConfirmEmail)
|
|
}
|
|
}
|
|
|
|
// RegisterChatwootAuthRoutes maps the root auth routes consumed by the Chatwoot frontend.
|
|
func RegisterChatwootAuthRoutes(rg *gin.RouterGroup, handler *AuthHandler) {
|
|
rg.POST("/sign_in", handler.ChatwootSignIn)
|
|
rg.DELETE("/sign_out", handler.ChatwootSignOut)
|
|
rg.GET("/validate_token", handler.ChatwootValidateToken)
|
|
rg.POST("/password", handler.ResetPassword)
|
|
rg.PUT("/password", handler.ConfirmResetPassword)
|
|
rg.POST("/confirmation", handler.ChatwootConfirmEmail)
|
|
}
|
|
|
|
func (h *AuthHandler) chatwootUserPayload(c *gin.Context, userID uint, accountID uint) (any, error) {
|
|
if h.profileService == nil {
|
|
return gin.H{"id": userID, "account_id": accountID}, nil
|
|
}
|
|
return h.profileService.Get(c.Request.Context(), userID, accountID)
|
|
}
|
|
|
|
func (h *AuthHandler) setChatwootAuthHeaders(c *gin.Context, output *service.LoginOutput) {
|
|
c.Header("token-type", "Bearer")
|
|
if output == nil || output.User == nil {
|
|
return
|
|
}
|
|
c.Header("uid", output.User.Email)
|
|
if output.TokenPair == nil {
|
|
return
|
|
}
|
|
c.Header("access-token", output.TokenPair.AccessToken)
|
|
clientID := output.ClientID
|
|
if clientID == "" {
|
|
clientID = output.TokenPair.RefreshToken
|
|
}
|
|
c.Header("client", clientID)
|
|
c.Header("expiry", strconv.FormatInt(output.TokenPair.ExpiresAt.Unix(), 10))
|
|
}
|
|
|
|
func (h *AuthHandler) trackChatwootSession(c *gin.Context, output *service.LoginOutput) error {
|
|
return h.authService.TrackChatwootSession(c.Request.Context(), output, c.GetHeader("client"), c.ClientIP(), c.GetHeader("User-Agent"))
|
|
}
|
|
|
|
func (h *AuthHandler) setBrowserSession(c *gin.Context, output *service.LoginOutput) {
|
|
if output == nil || output.TokenPair == nil || output.TokenPair.RefreshToken == "" {
|
|
return
|
|
}
|
|
c.SetSameSite(http.SameSiteLaxMode)
|
|
c.SetCookie(browserRefreshCookie, output.TokenPair.RefreshToken, 0, "/", "", h.secureCookies, true)
|
|
c.SetCookie(browserSessionMarker, "1", 0, "/", "", h.secureCookies, false)
|
|
c.Header("Cache-Control", "no-store")
|
|
}
|
|
|
|
func (h *AuthHandler) clearBrowserSession(c *gin.Context) {
|
|
c.SetSameSite(http.SameSiteLaxMode)
|
|
c.SetCookie(browserRefreshCookie, "", -1, "/", "", h.secureCookies, true)
|
|
c.SetCookie(browserSessionMarker, "", -1, "/", "", h.secureCookies, false)
|
|
c.Header("Cache-Control", "no-store")
|
|
}
|
|
|
|
func extractChatwootAccessToken(c *gin.Context) string {
|
|
if token := strings.TrimSpace(c.GetHeader("access-token")); token != "" {
|
|
return token
|
|
}
|
|
authHeader := strings.TrimSpace(c.GetHeader("Authorization"))
|
|
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
|
|
return strings.TrimSpace(authHeader[7:])
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// generateOAuthState creates a cryptographically random state token for CSRF protection.
|
|
// Used by OIDC and other auth flows. Production note: state should also be stored
|
|
// server-side (Redis) and validated on callback.
|
|
func generateOAuthState() string {
|
|
return "gochat_oauth_" + randomHex(16)
|
|
}
|
|
|
|
// randomHex generates n cryptographically random bytes as hex string.
|
|
func randomHex(n int) string {
|
|
b := make([]byte, n)
|
|
if _, err := crypto_rand.Read(b); err != nil {
|
|
panic(fmt.Sprintf("generate OAuth state: %v", err))
|
|
}
|
|
return fmt.Sprintf("%x", b)
|
|
}
|