Skip to content

Commit 073747e

Browse files
authored
Add option to skip signature verification (#821)
## Changes - Allow skipping signature verification for webhooks ## Motivation The signature returned with webhooks is calculated using a single channel secret. If the bot owner changes their channel secret, the signature for webhooks starts being calculated using the new channel secret. To avoid signature verification failures, the bot owner must update the channel secret on their server, which is used for signature verification. However, if there is a timing mismatch in the update—and such a mismatch is almost unavoidable—verification will fail during that period. In such cases, having an option to skip signature verification for webhooks would be a convenient way to avoid these issues. ## Related PRs - line-bot-sdk-java: line/line-bot-sdk-java#1635 - line-bot-sdk-go: line/line-bot-sdk-go#595
1 parent 850db1f commit 073747e

File tree

3 files changed

+199
-8
lines changed

3 files changed

+199
-8
lines changed

README.rst

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,18 @@ WebhookParser
111111

112112
※ You can use WebhookParser
113113

114-
\_\_init\_\_(self, channel\_secret)
115-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
114+
\_\_init\_\_(self, channel\_secret, skip\_signature\_verification=lambda: False)
115+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
116116

117117
.. code:: python
118118
119119
parser = linebot.v3.WebhookParser('YOUR_CHANNEL_SECRET')
120120
121+
# or with skip_signature_verification option
122+
parser = linebot.v3.WebhookParser(
123+
'YOUR_CHANNEL_SECRET',
124+
skip_signature_verification=lambda: False
125+
121126
parse(self, body, signature, as_payload=False)
122127
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
123128
@@ -143,13 +148,18 @@ WebhookHandler
143148
144149
※ You can use WebhookHandler
145150
146-
\_\_init\_\_(self, channel\_secret)
147-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
151+
\_\_init\_\_(self, channel\_secret, skip\_signature\_verification=lambda: False)
152+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
148153
149154
.. code:: python
150155
151156
handler = linebot.v3.WebhookHandler('YOUR_CHANNEL_SECRET')
152157
158+
# or with skip_signature_verification option
159+
handler = linebot.v3.WebhookHandler(
160+
'YOUR_CHANNEL_SECRET',
161+
skip_signature_verification=lambda: False
162+
153163
handle(self, body, signature)
154164
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
155165

linebot/v3/webhook.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,20 @@ def __init__(self, events=None, destination=None):
112112
class WebhookParser(object):
113113
"""Webhook Parser."""
114114

115-
def __init__(self, channel_secret):
115+
def __init__(self, channel_secret, skip_signature_verification = lambda: False):
116116
"""__init__ method.
117117
118118
:param str channel_secret: Channel secret (as text)
119+
:param skip_signature_verification: (optional) Function that determines
120+
whether to skip webhook signature verification.
121+
122+
If this function returns True, the signature verification step is skipped.
123+
This can be useful in scenarios such as when you're in the process of
124+
updating the channel secret and need to temporarily bypass verification
125+
to avoid disruptions.
119126
"""
120127
self.signature_validator = SignatureValidator(channel_secret)
128+
self.skip_signature_verification = skip_signature_verification
121129

122130
def parse(self, body, signature, as_payload=False):
123131
"""Parse webhook request body as text.
@@ -129,7 +137,7 @@ def parse(self, body, signature, as_payload=False):
129137
| :py:class:`linebot.v3.webhook.WebhookPayload`
130138
:return: Events list, or WebhookPayload instance
131139
"""
132-
if not self.signature_validator.validate(body, signature):
140+
if not self.skip_signature_verification() and not self.signature_validator.validate(body, signature):
133141
raise InvalidSignatureError(
134142
'Invalid signature. signature=' + signature)
135143

@@ -154,12 +162,19 @@ class WebhookHandler(object):
154162
Please read https://github.com/line/line-bot-sdk-python#webhookhandler
155163
"""
156164

157-
def __init__(self, channel_secret):
165+
def __init__(self, channel_secret, skip_signature_verification = lambda: False):
158166
"""__init__ method.
159167
160168
:param str channel_secret: Channel secret (as text)
169+
:param skip_signature_verification: (optional) Function that determines
170+
whether to skip webhook signature verification.
171+
172+
If this function returns True, the signature verification step is skipped.
173+
This can be useful in scenarios such as when you're in the process of
174+
updating the channel secret and need to temporarily bypass verification
175+
to avoid disruptions.
161176
"""
162-
self.parser = WebhookParser(channel_secret)
177+
self.parser = WebhookParser(channel_secret, skip_signature_verification)
163178
self._handlers = {}
164179
self._default = None
165180

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from __future__ import unicode_literals, absolute_import
16+
17+
import unittest
18+
19+
from linebot.v3 import (
20+
WebhookParser, WebhookHandler
21+
)
22+
from linebot.v3.exceptions import InvalidSignatureError
23+
24+
25+
class TestWebhookParserWithSkipSignatureVerification(unittest.TestCase):
26+
def setUp(self):
27+
self.parser = WebhookParser('channel_secret')
28+
self.parser_with_skip = WebhookParser('channel_secret', skip_signature_verification=lambda: True)
29+
30+
def test_parse_with_invalid_signature(self):
31+
body = '{"events": []}'
32+
signature = 'invalid_signature'
33+
34+
# Should raise InvalidSignatureError when skip_signature_verification is False (default)
35+
with self.assertRaises(InvalidSignatureError):
36+
self.parser.parse(body, signature)
37+
38+
# Should not raise InvalidSignatureError when skip_signature_verification is True
39+
try:
40+
self.parser_with_skip.parse(body, signature)
41+
except InvalidSignatureError:
42+
self.fail("parse() raised InvalidSignatureError unexpectedly!")
43+
44+
45+
class TestWebhookHandlerWithSkipSignatureVerification(unittest.TestCase):
46+
def setUp(self):
47+
self.handler = WebhookHandler('channel_secret')
48+
self.handler_with_skip = WebhookHandler('channel_secret', skip_signature_verification=lambda: True)
49+
self.handler_called = False
50+
self.handler_with_skip_called = False
51+
52+
@self.handler.default()
53+
def default(event):
54+
self.handler_called = True
55+
56+
@self.handler_with_skip.default()
57+
def default_with_skip(event):
58+
self.handler_with_skip_called = True
59+
60+
def test_handle_with_invalid_signature(self):
61+
body = """
62+
{
63+
"events": [
64+
{
65+
"type": "message",
66+
"message": {
67+
"type": "text",
68+
"id": "123",
69+
"text": "test"
70+
},
71+
"timestamp": 1462629479859,
72+
"source": {
73+
"type": "user",
74+
"userId": "U123"
75+
},
76+
"replyToken": "reply_token",
77+
"mode": "active",
78+
"webhookEventId": "test_id",
79+
"deliveryContext": {
80+
"isRedelivery": false
81+
}
82+
}
83+
]
84+
}
85+
"""
86+
signature = 'invalid_signature'
87+
88+
# Should raise InvalidSignatureError when skip_signature_verification is False (default)
89+
with self.assertRaises(InvalidSignatureError):
90+
self.handler.handle(body, signature)
91+
92+
# Handler should not be called when signature verification fails
93+
self.assertFalse(self.handler_called)
94+
95+
# Should not raise InvalidSignatureError when skip_signature_verification is True
96+
try:
97+
self.handler_with_skip.handle(body, signature)
98+
except InvalidSignatureError:
99+
self.fail("handle() raised InvalidSignatureError unexpectedly!")
100+
101+
# Handler should be called when signature verification is skipped
102+
self.assertTrue(self.handler_with_skip_called)
103+
104+
def test_dynamic_skip_signature_verification(self):
105+
body = """
106+
{
107+
"events": [
108+
{
109+
"type": "message",
110+
"message": {
111+
"type": "text",
112+
"id": "123",
113+
"text": "test"
114+
},
115+
"timestamp": 1462629479859,
116+
"source": {
117+
"type": "user",
118+
"userId": "U123"
119+
},
120+
"replyToken": "reply_token",
121+
"mode": "active",
122+
"webhookEventId": "test_id",
123+
"deliveryContext": {
124+
"isRedelivery": false
125+
}
126+
}
127+
]
128+
}
129+
"""
130+
signature = 'invalid_signature'
131+
skip_flag = [False]
132+
133+
# Create a handler with dynamic skip flag
134+
handler_with_dynamic_skip = WebhookHandler(
135+
'channel_secret',
136+
skip_signature_verification=lambda: skip_flag[0]
137+
)
138+
139+
dynamic_handler_called = [False]
140+
141+
@handler_with_dynamic_skip.default()
142+
def default_dynamic(event):
143+
dynamic_handler_called[0] = True
144+
145+
# Should raise InvalidSignatureError when skip_signature_verification returns False
146+
with self.assertRaises(InvalidSignatureError):
147+
handler_with_dynamic_skip.handle(body, signature)
148+
149+
# Handler should not be called
150+
self.assertFalse(dynamic_handler_called[0])
151+
152+
# Change the skip flag
153+
skip_flag[0] = True
154+
155+
# Should not raise InvalidSignatureError now
156+
try:
157+
handler_with_dynamic_skip.handle(body, signature)
158+
except InvalidSignatureError:
159+
self.fail("handle() raised InvalidSignatureError unexpectedly!")
160+
161+
# Handler should be called now
162+
self.assertTrue(dynamic_handler_called[0])
163+
164+
165+
if __name__ == '__main__':
166+
unittest.main()

0 commit comments

Comments
 (0)