From b44e4713d850e26c2a726d2858e76d40425fd547 Mon Sep 17 00:00:00 2001 From: habara keigo Date: Fri, 27 Jun 2025 18:01:55 +0900 Subject: [PATCH 1/5] Add option to skip signature verification --- examples/echo_bot/server.go | 6 ++++-- linebot/webhook/parse.go | 13 +++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/echo_bot/server.go b/examples/echo_bot/server.go index 702d18d2..275ffe96 100644 --- a/examples/echo_bot/server.go +++ b/examples/echo_bot/server.go @@ -26,7 +26,7 @@ import ( ) func main() { - channelSecret := os.Getenv("LINE_CHANNEL_SECRET") + channelSecret := "DummyChannelSecret" // Dummy value is fine to skip webhook signature verification bot, err := messaging_api.NewMessagingApiAPI( os.Getenv("LINE_CHANNEL_TOKEN"), ) @@ -38,7 +38,9 @@ func main() { http.HandleFunc("/callback", func(w http.ResponseWriter, req *http.Request) { log.Println("/callback called...") - cb, err := webhook.ParseRequest(channelSecret, req) + cb, err := webhook.ParseRequestWithOption(channelSecret, req, &webhook.ParseOption{ + SkipSignatureValidation: func() bool { return true }, + }) if err != nil { log.Printf("Cannot parse request: %+v\n", err) if errors.Is(err, webhook.ErrInvalidSignature) { diff --git a/linebot/webhook/parse.go b/linebot/webhook/parse.go index b9093994..03c9ad17 100644 --- a/linebot/webhook/parse.go +++ b/linebot/webhook/parse.go @@ -15,14 +15,19 @@ var ( ErrInvalidSignature = errors.New("invalid signature") ) +type ParseOption struct { + SkipSignatureValidation func() bool +} + // ParseRequest func -func ParseRequest(channelSecret string, r *http.Request) (*CallbackRequest, error) { +func ParseRequestWithOption(channelSecret string, r *http.Request, opt *ParseOption) (*CallbackRequest, error) { defer func() { _ = r.Body.Close() }() body, err := io.ReadAll(r.Body) if err != nil { return nil, err } - if !ValidateSignature(channelSecret, r.Header.Get("x-line-signature"), body) { + skip := opt != nil && opt.SkipSignatureValidation != nil && opt.SkipSignatureValidation() + if !skip && !ValidateSignature(channelSecret, r.Header.Get("x-line-signature"), body) { return nil, ErrInvalidSignature } @@ -33,6 +38,10 @@ func ParseRequest(channelSecret string, r *http.Request) (*CallbackRequest, erro return &cb, nil } +func ParseRequest(channelSecret string, r *http.Request) (*CallbackRequest, error) { + return ParseRequestWithOption(channelSecret, r, nil) +} + func ValidateSignature(channelSecret, signature string, body []byte) bool { decoded, err := base64.StdEncoding.DecodeString(signature) if err != nil { From fb618dd421db566bca3a92ab9d8c15005dd664ef Mon Sep 17 00:00:00 2001 From: habara keigo Date: Fri, 4 Jul 2025 15:35:53 +0900 Subject: [PATCH 2/5] Revert examples --- examples/echo_bot/server.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/echo_bot/server.go b/examples/echo_bot/server.go index 275ffe96..702d18d2 100644 --- a/examples/echo_bot/server.go +++ b/examples/echo_bot/server.go @@ -26,7 +26,7 @@ import ( ) func main() { - channelSecret := "DummyChannelSecret" // Dummy value is fine to skip webhook signature verification + channelSecret := os.Getenv("LINE_CHANNEL_SECRET") bot, err := messaging_api.NewMessagingApiAPI( os.Getenv("LINE_CHANNEL_TOKEN"), ) @@ -38,9 +38,7 @@ func main() { http.HandleFunc("/callback", func(w http.ResponseWriter, req *http.Request) { log.Println("/callback called...") - cb, err := webhook.ParseRequestWithOption(channelSecret, req, &webhook.ParseOption{ - SkipSignatureValidation: func() bool { return true }, - }) + cb, err := webhook.ParseRequest(channelSecret, req) if err != nil { log.Printf("Cannot parse request: %+v\n", err) if errors.Is(err, webhook.ErrInvalidSignature) { From f19035a26b33ed2955b9babdceaf30d6deb6cb9c Mon Sep 17 00:00:00 2001 From: habara keigo Date: Fri, 4 Jul 2025 15:46:24 +0900 Subject: [PATCH 3/5] Add comment to parse.go --- linebot/webhook/parse.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/linebot/webhook/parse.go b/linebot/webhook/parse.go index 03c9ad17..275055ae 100644 --- a/linebot/webhook/parse.go +++ b/linebot/webhook/parse.go @@ -16,10 +16,21 @@ var ( ) type ParseOption struct { + // SkipSignatureValidation is a function that determines whether to skip + // webhook signature verification. + // + // If the function returns true, the signature verification step is skipped. + // This can be useful in scenarios such as when you're in the process of updating + // the channel secret and need to temporarily bypass verification to avoid disruptions. SkipSignatureValidation func() bool } -// ParseRequest func +// ParseRequestWithOption parses a LINE webhook request with optional behavior. +// +// Use this when you need to customize parsing, such as skipping signature validation +// via ParseOption. This is useful during channel secret rotation or local development. +// +// For standard use, prefer ParseRequest. func ParseRequestWithOption(channelSecret string, r *http.Request, opt *ParseOption) (*CallbackRequest, error) { defer func() { _ = r.Body.Close() }() body, err := io.ReadAll(r.Body) @@ -38,6 +49,10 @@ func ParseRequestWithOption(channelSecret string, r *http.Request, opt *ParseOpt return &cb, nil } +// ParseRequest parses a LINE webhook request with signature verification. +// +// If you need to customize behavior (e.g. skip signature verification), +// use ParseRequestWithOption instead. func ParseRequest(channelSecret string, r *http.Request) (*CallbackRequest, error) { return ParseRequestWithOption(channelSecret, r, nil) } From fa77c7082f99ed383011dc49e2a50017f65c396a Mon Sep 17 00:00:00 2001 From: habara keigo Date: Fri, 4 Jul 2025 16:06:31 +0900 Subject: [PATCH 4/5] Add test --- .../tests/handwritten/model_source_test.go | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/linebot/webhook/tests/handwritten/model_source_test.go b/linebot/webhook/tests/handwritten/model_source_test.go index f6fe21ce..2f2966c4 100644 --- a/linebot/webhook/tests/handwritten/model_source_test.go +++ b/linebot/webhook/tests/handwritten/model_source_test.go @@ -1,7 +1,14 @@ package tests import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "crypto/tls" + "encoding/base64" "encoding/json" + "net/http" + "net/http/httptest" "testing" "github.com/line/line-bot-sdk-go/v8/linebot/webhook" @@ -26,3 +33,95 @@ func TestStickerMessage(t *testing.T) { t.Fatalf("Failed to cast to UnknownEvent: %v", cb.Events[0]) } } + +func generateSignature(secret string, body []byte) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write(body) + return base64.StdEncoding.EncodeToString(mac.Sum(nil)) +} + +func makeRequest(t *testing.T, url string, body []byte, signature string) *http.Request { + req, err := http.NewRequest("POST", url, bytes.NewReader(body)) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("X-Line-Signature", signature) + return req +} + +func TestWebhookParseRequestWithOption(t *testing.T) { + const channelSecret = "testsecret" + body := []byte(`{"destination":"U0123456789abcdef","events":[]}`) + + tests := []struct { + name string + skipValidation bool + useValidSig bool + expectedCode int + }{ + { + name: "valid signature, no skip", + skipValidation: false, + useValidSig: true, + expectedCode: http.StatusOK, + }, + { + name: "invalid signature, no skip", + skipValidation: false, + useValidSig: false, + expectedCode: http.StatusBadRequest, + }, + { + name: "invalid signature, but skip = true", + skipValidation: true, + useValidSig: false, + expectedCode: http.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + opt := &webhook.ParseOption{ + SkipSignatureValidation: func() bool { return tt.skipValidation }, + } + cb, err := webhook.ParseRequestWithOption(channelSecret, req, opt) + if err != nil { + if err == webhook.ErrInvalidSignature { + w.WriteHeader(http.StatusBadRequest) + return + } + t.Errorf("unexpected error: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + if cb.Destination != "U0123456789abcdef" { + t.Errorf("destination = %s; want %s", cb.Destination, "U0123456789abcdef") + } + w.WriteHeader(http.StatusOK) + }) + + server := httptest.NewTLSServer(handler) + defer server.Close() + + signature := "invalid" + if tt.useValidSig { + signature = generateSignature(channelSecret, body) + } + req := makeRequest(t, server.URL, body, signature) + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + res, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + if res.StatusCode != tt.expectedCode { + t.Errorf("StatusCode = %d; want %d", res.StatusCode, tt.expectedCode) + } + }) + } +} From d12c7dffc7d4e270c9c3fe1207feec9d15f999ea Mon Sep 17 00:00:00 2001 From: habara keigo Date: Fri, 25 Jul 2025 11:35:41 +0900 Subject: [PATCH 5/5] Add before-spec test --- .../tests/handwritten/model_source_test.go | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/linebot/webhook/tests/handwritten/model_source_test.go b/linebot/webhook/tests/handwritten/model_source_test.go index 2f2966c4..b253bdcf 100644 --- a/linebot/webhook/tests/handwritten/model_source_test.go +++ b/linebot/webhook/tests/handwritten/model_source_test.go @@ -49,6 +49,47 @@ func makeRequest(t *testing.T, url string, body []byte, signature string) *http. return req } +func TestWebhookParseRequest(t *testing.T) { + const channelSecret = "testsecret" + body := []byte(`{"destination":"U0123456789abcdef","events":[]}`) + + handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + cb, err := webhook.ParseRequest(channelSecret, req) + if err != nil { + if err == webhook.ErrInvalidSignature { + w.WriteHeader(http.StatusBadRequest) + return + } + t.Errorf("unexpected error: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + if cb.Destination != "U0123456789abcdef" { + t.Errorf("destination = %s; want %s", cb.Destination, "U0123456789abcdef") + } + w.WriteHeader(http.StatusOK) + }) + + server := httptest.NewTLSServer(handler) + defer server.Close() + + signature := generateSignature(channelSecret, body) + req := makeRequest(t, server.URL, body, signature) + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + res, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("StatusCode = %d; want %d", res.StatusCode, http.StatusOK) + } +} + func TestWebhookParseRequestWithOption(t *testing.T) { const channelSecret = "testsecret" body := []byte(`{"destination":"U0123456789abcdef","events":[]}`)