Skip to content

Add option to skip signature verification #595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions linebot/webhook/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,30 @@ var (
ErrInvalidSignature = errors.New("invalid signature")
)

// ParseRequest func
func ParseRequest(channelSecret string, r *http.Request) (*CallbackRequest, error) {
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
}
Comment on lines +18 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When adding another option later, would it always require a major version upgrade? I think it's better to ensure the code works seamlessly with users' existing builds, even when adding another option.

Copy link
Contributor Author

@habara-k habara-k Jul 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure this change doesn't break existing code that uses the current line-bot-sdk-go, I also added tests for the existing ParseRequest (without options). d12c7df

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you.

Once again: Would adding an option in another change cause any issues in your land(like compile fails)?

type ParseOption struct {
	SkipSignatureValidation func() bool
+      SomethingNew func() bool // Adding this later may cause compile error in your land? or not?
}


// 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)
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
}

Expand All @@ -33,6 +49,14 @@ func ParseRequest(channelSecret string, r *http.Request) (*CallbackRequest, erro
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)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do these like the Java SDK?

  • Write comments explaining why users need this option in ParseRequestWithOption /when it should be used.
  • Indicate to ParseRequest users that there is a version where the option can be utilized.
  • Write sufficient tests.

func ValidateSignature(channelSecret, signature string, body []byte) bool {
decoded, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
Expand Down
140 changes: 140 additions & 0 deletions linebot/webhook/tests/handwritten/model_source_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -26,3 +33,136 @@ 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 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":[]}`)

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)
}
})
}
}