Skip to content

Commit 7736bcb

Browse files
Merge pull request #1 from ericbrisrubio/feature/Adding-secure-verification-methods
Adding methods for signature verification
2 parents 9e5c04e + 6706e50 commit 7736bcb

File tree

4 files changed

+273
-0
lines changed

4 files changed

+273
-0
lines changed

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A Go SDK client for interacting with the messages-worker service. This SDK provi
77
- **Message Operations**: Submit single or bulk messages with different priorities
88
- **Worker Management**: Monitor and scale workers dynamically
99
- **Health Checks**: Check service health and availability
10+
- **Callback Signature Verification**: Verify HMAC-SHA256 signatures for secure callback handling
1011
- **Error Handling**: Comprehensive error handling with custom error types
1112
- **Context Support**: Full context.Context support for timeouts and cancellation
1213
- **Type Safety**: Strongly typed API with proper validation
@@ -248,11 +249,70 @@ defer cancel()
248249
resp, err := client.PostMessage(ctx, messageReq)
249250
```
250251

252+
## Callback Signature Verification
253+
254+
The messages-worker can generate HMAC-SHA256 signatures for callback requests. Use the SDK to verify these signatures:
255+
256+
```go
257+
import (
258+
"io"
259+
"github.com/ericbrisrubio/messages-worker-sdk"
260+
)
261+
262+
func handleCallback(w http.ResponseWriter, r *http.Request) {
263+
secret := os.Getenv("CALLBACK_SECRET")
264+
265+
// Read the request body
266+
body, err := io.ReadAll(r.Body)
267+
if err != nil {
268+
log.Printf("Failed to read request body: %v", err)
269+
http.Error(w, "Failed to read request body", http.StatusBadRequest)
270+
return
271+
}
272+
273+
// Get signature from header
274+
signature := r.Header.Get(sdk.GetSignatureHeader())
275+
276+
// Verify signature
277+
valid, err := sdk.VerifyCallbackSignature(body, signature, secret)
278+
if err != nil {
279+
log.Printf("Signature verification error: %v", err)
280+
http.Error(w, "Signature verification failed", http.StatusUnauthorized)
281+
return
282+
}
283+
if !valid {
284+
log.Printf("Invalid signature")
285+
http.Error(w, "Invalid signature", http.StatusUnauthorized)
286+
return
287+
}
288+
289+
// Process callback...
290+
log.Println("Signature verified successfully")
291+
}
292+
```
293+
294+
### Signature Header
295+
296+
The messages-worker sends signatures in the `X-Callback-Signature` header:
297+
298+
```
299+
X-Callback-Signature: <hmac-sha256-hex-signature>
300+
```
301+
302+
### Configuration
303+
304+
Set the `CALLBACK_SECRET` environment variable in your messages-worker configuration:
305+
306+
```bash
307+
export CALLBACK_SECRET="your-secret-key-here"
308+
```
309+
251310
## Examples
252311

253312
See the `examples/` directory for complete working examples:
254313

255314
- `examples/main.go`: Comprehensive example showing all SDK features
315+
- `examples/callback_handler.go`: Complete callback handler with signature verification
256316

257317
## API Reference
258318

examples/callback-handler/main.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"log"
7+
"net/http"
8+
"os"
9+
10+
"github.com/ericbrisrubio/messages-worker-sdk"
11+
)
12+
13+
// CallbackHandler handles incoming callbacks from messages-worker
14+
type CallbackHandler struct {
15+
secret string
16+
}
17+
18+
// NewCallbackHandler creates a new callback handler
19+
func NewCallbackHandler(secret string) *CallbackHandler {
20+
return &CallbackHandler{
21+
secret: secret,
22+
}
23+
}
24+
25+
// HandleCallback processes incoming callback requests
26+
func (h *CallbackHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
27+
// Read the request body
28+
body, err := io.ReadAll(r.Body)
29+
if err != nil {
30+
log.Printf("Failed to read request body: %v", err)
31+
http.Error(w, "Failed to read request body", http.StatusBadRequest)
32+
return
33+
}
34+
35+
// Verify signature if secret is configured
36+
if h.secret != "" {
37+
signature := r.Header.Get(sdk.GetSignatureHeader())
38+
valid := sdk.VerifySignatureSha256(body, signature, h.secret)
39+
if !valid {
40+
log.Printf("Invalid signature")
41+
http.Error(w, "Invalid signature", http.StatusUnauthorized)
42+
return
43+
}
44+
log.Printf("Signature verified successfully")
45+
}
46+
47+
// Parse the message
48+
var message map[string]interface{}
49+
if err := json.Unmarshal(body, &message); err != nil {
50+
log.Printf("Failed to decode message: %v", err)
51+
http.Error(w, "Invalid JSON", http.StatusBadRequest)
52+
return
53+
}
54+
55+
// Process the message
56+
log.Printf("Received callback message: %+v", message)
57+
58+
// Return success response
59+
w.Header().Set("Content-Type", "application/json")
60+
w.WriteHeader(http.StatusOK)
61+
json.NewEncoder(w).Encode(map[string]string{
62+
"status": "success",
63+
"message": "Callback processed successfully",
64+
})
65+
}
66+
67+
func main() {
68+
// Get callback secret from environment
69+
secret := os.Getenv("CALLBACK_SECRET")
70+
if secret == "" {
71+
log.Println("Warning: CALLBACK_SECRET not set, callbacks will not be verified")
72+
}
73+
74+
// Create callback handler
75+
handler := NewCallbackHandler(secret)
76+
77+
// Setup routes
78+
http.HandleFunc("/callback", handler.HandleCallback)
79+
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
80+
w.WriteHeader(http.StatusOK)
81+
w.Write([]byte("OK"))
82+
})
83+
84+
// Start server
85+
port := os.Getenv("PORT")
86+
if port == "" {
87+
port = "8080"
88+
}
89+
90+
log.Printf("Starting callback server on port %s", port)
91+
log.Fatal(http.ListenAndServe(":"+port, nil))
92+
}

signature.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package sdk
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"strings"
8+
)
9+
10+
// VerifyCallbackSignature verifies the callback signature using HMAC-SHA256
11+
func VerifySignatureSha256(payload []byte, signature string, webhookSecret string) bool {
12+
if signature == "" {
13+
return false
14+
}
15+
16+
// Remove "sha256=" prefix if present
17+
signature = strings.TrimPrefix(signature, "sha256=")
18+
19+
// Create HMAC-SHA256 hash
20+
mac := hmac.New(sha256.New, []byte(webhookSecret))
21+
mac.Write(payload)
22+
expectedMAC := mac.Sum(nil)
23+
24+
// Convert to hex string
25+
expectedSignature := hex.EncodeToString(expectedMAC)
26+
27+
// Compare signatures using constant time comparison
28+
return hmac.Equal([]byte(signature), []byte(expectedSignature))
29+
}
30+
31+
// GetSignatureHeader returns the header name used for callback signatures
32+
func GetSignatureHeader() string {
33+
return "X-Callback-Signature"
34+
}

signature_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package sdk
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"testing"
8+
)
9+
10+
func TestVerifyCallbackSignature(t *testing.T) {
11+
secret := "test-secret"
12+
payload := `{"id":"test-id","item_id":"pr-123","priority":"high"}`
13+
14+
tests := []struct {
15+
name string
16+
payload string
17+
signature string
18+
secret string
19+
expected bool
20+
expectedError bool
21+
}{
22+
{
23+
name: "valid signature",
24+
payload: payload,
25+
signature: generateTestSignature(payload, secret),
26+
secret: secret,
27+
expected: true,
28+
expectedError: false,
29+
},
30+
{
31+
name: "missing signature",
32+
payload: payload,
33+
signature: "",
34+
secret: secret,
35+
expected: false,
36+
expectedError: true,
37+
},
38+
{
39+
name: "invalid signature",
40+
payload: payload,
41+
signature: "invalidhash1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
42+
secret: secret,
43+
expected: false,
44+
expectedError: false,
45+
},
46+
{
47+
name: "empty secret",
48+
payload: payload,
49+
signature: generateTestSignature(payload, secret),
50+
secret: "",
51+
expected: false,
52+
expectedError: true,
53+
},
54+
}
55+
56+
for _, tt := range tests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
result := VerifySignatureSha256([]byte(tt.payload), tt.signature, tt.secret)
59+
60+
if tt.expectedError {
61+
if result == true {
62+
t.Errorf("VerifyCallbackSignature() expected error, got nil")
63+
}
64+
return
65+
}
66+
67+
if result != tt.expected {
68+
t.Errorf("VerifyCallbackSignature() = %v, expected %v", result, tt.expected)
69+
}
70+
})
71+
}
72+
}
73+
74+
func TestGetSignatureHeader(t *testing.T) {
75+
expected := "X-Callback-Signature"
76+
result := GetSignatureHeader()
77+
if result != expected {
78+
t.Errorf("GetSignatureHeader() = %v, expected %v", result, expected)
79+
}
80+
}
81+
82+
// Helper function to generate test signatures
83+
func generateTestSignature(payload, secret string) string {
84+
mac := hmac.New(sha256.New, []byte(secret))
85+
mac.Write([]byte(payload))
86+
return hex.EncodeToString(mac.Sum(nil))
87+
}

0 commit comments

Comments
 (0)