From b5ac10871968bea080d1bb92414bc0e9abac28b6 Mon Sep 17 00:00:00 2001 From: iamatulsingh Date: Sun, 8 Mar 2026 21:00:28 +0100 Subject: [PATCH] feat: JWT authentication middleware and login endpoint --- whatsapp-mcp-server/.idea/vcs.xml | 2 +- whatsapp-mcp-server/go.mod | 1 + whatsapp-mcp-server/helpers/api_auth.go | 77 +++++++++++++++++++++++++ whatsapp-mcp-server/helpers/mcp_tool.go | 9 ++- 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 whatsapp-mcp-server/helpers/api_auth.go diff --git a/whatsapp-mcp-server/.idea/vcs.xml b/whatsapp-mcp-server/.idea/vcs.xml index 4fce1d8..6c0b863 100644 --- a/whatsapp-mcp-server/.idea/vcs.xml +++ b/whatsapp-mcp-server/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/whatsapp-mcp-server/go.mod b/whatsapp-mcp-server/go.mod index 169a1de..317334c 100644 --- a/whatsapp-mcp-server/go.mod +++ b/whatsapp-mcp-server/go.mod @@ -3,6 +3,7 @@ module whatsapp-mcp-server go 1.25.0 require ( + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/mattn/go-sqlite3 v1.14.34 github.com/modelcontextprotocol/go-sdk v1.3.1 ) diff --git a/whatsapp-mcp-server/helpers/api_auth.go b/whatsapp-mcp-server/helpers/api_auth.go new file mode 100644 index 0000000..b2c7349 --- /dev/null +++ b/whatsapp-mcp-server/helpers/api_auth.go @@ -0,0 +1,77 @@ +package helpers + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "strings" + "sync" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var ( + apiSecret = ReadEnv("WHATSAPP_API_SECRET", "") + jwtToken string + tokenMutex sync.Mutex + tokenExpiresAt time.Time +) + +// GetOrRefreshJwtToken returns a valid JWT or fetches a new one +func GetOrRefreshJwtToken() (string, error) { + tokenMutex.Lock() + defer tokenMutex.Unlock() + + // Reuse token if still valid + if jwtToken != "" && time.Now().Before(tokenExpiresAt.Add(-30*time.Second)) { + return jwtToken, nil + } + + // Request new token + req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s", strings.TrimSuffix(apiBaseURL, "/api"), "auth/login"), nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiSecret)) + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("auth request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("auth failed %d: %s", resp.StatusCode, string(body)) + } + + var data struct { + Token string `json:"token"` + } + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return "", fmt.Errorf("failed to decode token response: %w", err) + } + + if data.Token == "" { + return "", fmt.Errorf("no token returned from /auth/login") + } + + // Parse WITHOUT verifying signature + parsed, _, err := jwt.NewParser().ParseUnverified(data.Token, jwt.MapClaims{}) + if err == nil { + if claims, ok := parsed.Claims.(jwt.MapClaims); ok { + if exp, ok := claims["exp"].(float64); ok { + tokenExpiresAt = time.Unix(int64(exp), 0) + } + } + } + + jwtToken = data.Token + slog.Info("Fetched new JWT token", "expires", tokenExpiresAt) + + return jwtToken, nil +} diff --git a/whatsapp-mcp-server/helpers/mcp_tool.go b/whatsapp-mcp-server/helpers/mcp_tool.go index 3aeb8cc..96e18ba 100644 --- a/whatsapp-mcp-server/helpers/mcp_tool.go +++ b/whatsapp-mcp-server/helpers/mcp_tool.go @@ -179,6 +179,11 @@ type downloadMediaInput struct { } func callAPI(method, path string, body any) ([]byte, error) { + token, err := GetOrRefreshJwtToken() + if err != nil { + return nil, fmt.Errorf("authentication failed: %w", err) + } + var reqBody io.Reader if body != nil { b, err := json.Marshal(body) @@ -188,12 +193,14 @@ func callAPI(method, path string, body any) ([]byte, error) { reqBody = bytes.NewReader(b) } - req, err := http.NewRequest(method, apiBaseURL+path, reqBody) + fullURL := fmt.Sprintf("%s%s", apiBaseURL, path) + req, err := http.NewRequest(method, fullURL, reqBody) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) client := &http.Client{Timeout: apiTimeout} resp, err := client.Do(req)