Skip to content

Commit 9eb91a6

Browse files
authored
Allow to skip signature verification (#664)
## 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.
1 parent eb71cdb commit 9eb91a6

File tree

3 files changed

+103
-8
lines changed

3 files changed

+103
-8
lines changed

lib/line/bot/v2/webhook_parser.rb

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,20 @@ module V2
1111
class WebhookParser
1212
class InvalidSignatureError < StandardError; end
1313

14-
def initialize(channel_secret:)
14+
# Initialize webhook parser
15+
#
16+
# @param channel_secret [String]
17+
# The channel secret used for signature verification.
18+
# @param skip_signature_verification [() -> bool, nil]
19+
# A callable object with type `() -> bool` that determines whether to skip
20+
# webhook signature verification. Signature verification is skipped if and
21+
# only if this callable is provided and returns `true`.
22+
# This can be useful in scenarios such as when you're in the process of
23+
# updating the channel secret and need to temporarily bypass verification
24+
# to avoid disruptions.
25+
def initialize(channel_secret:, skip_signature_verification: nil)
1526
@channel_secret = channel_secret
27+
@skip_signature_verification = skip_signature_verification
1628
end
1729

1830
# Parse events from the raw request body and validate the signature.
@@ -31,7 +43,10 @@ def initialize(channel_secret:)
3143
#
3244
# @example Sinatra usage
3345
# def parser
34-
# @parser ||= Line::Bot::V2::WebhookParser.new(channel_secret: ENV.fetch("LINE_CHANNEL_SECRET"))
46+
# @parser ||= Line::Bot::V2::WebhookParser.new(
47+
# channel_secret: ENV.fetch("LINE_CHANNEL_SECRET"),
48+
# skip_signature_verification: -> { ENV['SKIP_SIGNATURE_VERIFICATION'] == 'true' }
49+
# )
3550
# end
3651
#
3752
# post '/callback' do
@@ -54,7 +69,11 @@ def initialize(channel_secret:)
5469
# "OK"
5570
# end
5671
def parse(body:, signature:)
57-
raise InvalidSignatureError.new("Invalid signature: #{signature}") unless verify_signature(body: body, signature: signature)
72+
should_skip = @skip_signature_verification&.call || false
73+
74+
unless should_skip == true || verify_signature(body: body, signature: signature)
75+
raise InvalidSignatureError.new("Invalid signature: #{signature}")
76+
end
5877

5978
data = JSON.parse(body.chomp, symbolize_names: true)
6079
data = Line::Bot::V2::Utils.deep_underscore(data)
@@ -66,14 +85,14 @@ def parse(body:, signature:)
6685
end
6786
end
6887

69-
private
70-
7188
def verify_signature(body:, signature:)
7289
hash = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @channel_secret, body)
7390
expected = Base64.strict_encode64(hash)
7491
variable_secure_compare(signature, expected)
7592
end
7693

94+
private
95+
7796
# To avoid timing attacks
7897
def variable_secure_compare(a, b)
7998
secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b))

sig/line/bot/v2/webhook_parser.rbs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@ module Line
55
class InvalidSignatureError < ::StandardError
66
end
77
@channel_secret: String
8+
@skip_signature_verification: (^() -> bool) | nil
89

9-
def initialize: (channel_secret: String) -> void
10+
def initialize: (channel_secret: String, ?skip_signature_verification: (^() -> bool) | nil) -> void
1011

1112
def parse: (
1213
body: String,
1314
signature: String
1415
) -> Array[Webhook::Event]
16+
17+
def verify_signature: (body: String, signature: String) -> bool
1518

1619
private
1720

18-
def verify_signature: (body: String, signature: String) -> bool
19-
2021
def variable_secure_compare: (String, String) -> bool
2122

2223
def secure_compare: (String, String) -> bool
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
require 'spec_helper'
2+
3+
describe Line::Bot::V2::WebhookParser do
4+
let(:channel_secret) { 'dummy_channel_secret' }
5+
let(:signature) { 'invalid_signature' }
6+
let(:webhook) do
7+
<<~JSON
8+
{
9+
"destination": "xxxxxxxxxx",
10+
"events": [
11+
{
12+
"type": "message",
13+
"message": {
14+
"type": "text",
15+
"id": "123456789",
16+
"quoteToken": "q3Plxr4AgKd...",
17+
"text": "Hello, world"
18+
},
19+
"timestamp": 1462629479859,
20+
"source": {
21+
"type": "user",
22+
"userId": "U4af4980629..."
23+
},
24+
"webhookEventId": "01FZ74A0TDDPYRVKNK77XKC3ZR",
25+
"deliveryContext": {
26+
"isRedelivery": false
27+
},
28+
"replyToken": "fbf94e269485410da6b7e3a5e33283e8",
29+
"mode": "active"
30+
}
31+
]
32+
}
33+
JSON
34+
end
35+
36+
describe '#parse with skip_signature_verification' do
37+
context 'when skip_signature_verification is not provided' do
38+
let(:parser) { Line::Bot::V2::WebhookParser.new(channel_secret: channel_secret) }
39+
40+
it 'verifies the signature' do
41+
expect(parser).to receive(:verify_signature).and_return(false)
42+
expect { parser.parse(body: webhook, signature: signature) }.to raise_error(Line::Bot::V2::WebhookParser::InvalidSignatureError)
43+
end
44+
end
45+
46+
context 'when skip_signature_verification returns false' do
47+
let(:parser) { Line::Bot::V2::WebhookParser.new(channel_secret: channel_secret, skip_signature_verification: -> { false }) }
48+
49+
it 'verifies the signature' do
50+
expect(parser).to receive(:verify_signature).and_return(false)
51+
expect { parser.parse(body: webhook, signature: signature) }.to raise_error(Line::Bot::V2::WebhookParser::InvalidSignatureError)
52+
end
53+
end
54+
55+
context 'when skip_signature_verification returns true' do
56+
let(:parser) { Line::Bot::V2::WebhookParser.new(channel_secret: channel_secret, skip_signature_verification: -> { true }) }
57+
58+
it 'skips signature verification and parses the webhook' do
59+
expect(parser).not_to receive(:verify_signature)
60+
events = parser.parse(body: webhook, signature: signature)
61+
expect(events).not_to be_empty
62+
expect(events.first).to be_a(Line::Bot::V2::Webhook::MessageEvent)
63+
end
64+
end
65+
66+
context 'when skip_signature_verification is nil' do
67+
let(:parser) { Line::Bot::V2::WebhookParser.new(channel_secret: channel_secret, skip_signature_verification: nil) }
68+
69+
it 'verifies the signature' do
70+
expect(parser).to receive(:verify_signature).and_return(false)
71+
expect { parser.parse(body: webhook, signature: signature) }.to raise_error(Line::Bot::V2::WebhookParser::InvalidSignatureError)
72+
end
73+
end
74+
end
75+
end

0 commit comments

Comments
 (0)