Skip to content

Commit 9d9c22e

Browse files
authored
Add option to skip signature verification (#1635)
## 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 b778cd2 commit 9d9c22e

File tree

8 files changed

+175
-16
lines changed

8 files changed

+175
-16
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.bot.parser;
18+
19+
public class FixedSkipSignatureVerificationSupplier implements SkipSignatureVerificationSupplier {
20+
private final boolean fixedValue;
21+
22+
public FixedSkipSignatureVerificationSupplier(boolean fixedValue) {
23+
this.fixedValue = fixedValue;
24+
}
25+
26+
public static FixedSkipSignatureVerificationSupplier of(boolean fixedValue) {
27+
return new FixedSkipSignatureVerificationSupplier(fixedValue);
28+
}
29+
30+
@Override
31+
public boolean getAsBoolean() {
32+
return fixedValue;
33+
}
34+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.bot.parser;
18+
19+
import java.util.function.BooleanSupplier;
20+
21+
/**
22+
* Special {@link BooleanSupplier} for Skip Signature Verification.
23+
*
24+
* <p>You can implement it to return whether to skip signature verification.
25+
*
26+
* <p>If true is returned, webhook signature verification is skipped.
27+
* This may be helpful when you update the channel secret and want to skip the verification temporarily.
28+
*/
29+
@FunctionalInterface
30+
public interface SkipSignatureVerificationSupplier extends BooleanSupplier {
31+
}

line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class WebhookParser {
3434

3535
private final ObjectMapper objectMapper = ModelObjectMapper.createNewObjectMapper();
3636
private final SignatureValidator signatureValidator;
37+
private final SkipSignatureVerificationSupplier skipSignatureVerificationSupplier;
3738

3839
/**
3940
* Creates a new instance.
@@ -42,6 +43,19 @@ public class WebhookParser {
4243
*/
4344
public WebhookParser(SignatureValidator signatureValidator) {
4445
this.signatureValidator = requireNonNull(signatureValidator);
46+
this.skipSignatureVerificationSupplier = FixedSkipSignatureVerificationSupplier.of(false);
47+
}
48+
49+
/**
50+
* Creates a new instance.
51+
*
52+
* @param signatureValidator LINE messaging API's signature validator
53+
* @param skipSignatureVerificationSupplier Supplier to determine whether to skip signature verification
54+
*/
55+
public WebhookParser(SignatureValidator signatureValidator,
56+
SkipSignatureVerificationSupplier skipSignatureVerificationSupplier) {
57+
this.signatureValidator = requireNonNull(signatureValidator);
58+
this.skipSignatureVerificationSupplier = requireNonNull(skipSignatureVerificationSupplier);
4559
}
4660

4761
/**
@@ -62,7 +76,8 @@ public CallbackRequest handle(String signature, byte[] payload) throws IOExcepti
6276
log.debug("got: {}", new String(payload, StandardCharsets.UTF_8));
6377
}
6478

65-
if (!signatureValidator.validateSignature(payload, signature)) {
79+
if (!skipSignatureVerificationSupplier.getAsBoolean()
80+
&& !signatureValidator.validateSignature(payload, signature)) {
6681
throw new WebhookParseException("Invalid API signature");
6782
}
6883

line-bot-parser/src/test/java/com/linecorp/bot/parser/WebhookParserTest.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
import static org.mockito.Mockito.never;
22+
import static org.mockito.Mockito.verify;
2123
import static org.mockito.Mockito.when;
2224

2325
import java.io.InputStream;
@@ -52,7 +54,9 @@ public boolean validateSignature(byte[] content, String headerSignature) {
5254

5355
@BeforeEach
5456
public void before() {
55-
parser = new WebhookParser(signatureValidator);
57+
parser = new WebhookParser(
58+
signatureValidator,
59+
FixedSkipSignatureVerificationSupplier.of(false));
5660
}
5761

5862
@Test
@@ -106,4 +110,60 @@ public void testCallRequest() throws Exception {
106110
assertThat(messageEvent.timestamp()).isEqualTo(
107111
Instant.parse("2016-05-07T13:57:59.859Z").toEpochMilli());
108112
}
113+
114+
@Test
115+
public void testSkipSignatureVerification() throws Exception {
116+
final InputStream resource = getClass().getClassLoader().getResourceAsStream(
117+
"callback-request.json");
118+
final byte[] payload = resource.readAllBytes();
119+
120+
final var parser = new WebhookParser(
121+
signatureValidator,
122+
FixedSkipSignatureVerificationSupplier.of(true));
123+
124+
// assert no interaction with signatureValidator
125+
verify(signatureValidator, never()).validateSignature(payload, "SSSSIGNATURE");
126+
127+
final CallbackRequest callbackRequest = parser.handle("SSSSIGNATURE", payload);
128+
129+
assertThat(callbackRequest).isNotNull();
130+
131+
final List<Event> result = callbackRequest.events();
132+
133+
@SuppressWarnings("rawtypes")
134+
final MessageEvent messageEvent = (MessageEvent) result.get(0);
135+
final TextMessageContent text = (TextMessageContent) messageEvent.message();
136+
assertThat(text.text()).isEqualTo("Hello, world");
137+
138+
final String followedUserId = messageEvent.source().userId();
139+
assertThat(followedUserId).isEqualTo("u206d25c2ea6bd87c17655609a1c37cb8");
140+
assertThat(messageEvent.timestamp()).isEqualTo(
141+
Instant.parse("2016-05-07T13:57:59.859Z").toEpochMilli());
142+
}
143+
144+
@Test
145+
public void testWithoutSkipSignatureVerificationSupplierInConstructor() throws Exception {
146+
final InputStream resource = getClass().getClassLoader().getResourceAsStream(
147+
"callback-request.json");
148+
final byte[] payload = resource.readAllBytes();
149+
150+
when(signatureValidator.validateSignature(payload, "SSSSIGNATURE")).thenReturn(true);
151+
152+
final var parser = new WebhookParser(signatureValidator);
153+
final CallbackRequest callbackRequest = parser.handle("SSSSIGNATURE", payload);
154+
155+
assertThat(callbackRequest).isNotNull();
156+
157+
final List<Event> result = callbackRequest.events();
158+
159+
@SuppressWarnings("rawtypes")
160+
final MessageEvent messageEvent = (MessageEvent) result.get(0);
161+
final TextMessageContent text = (TextMessageContent) messageEvent.message();
162+
assertThat(text.text()).isEqualTo("Hello, world");
163+
164+
final String followedUserId = messageEvent.source().userId();
165+
assertThat(followedUserId).isEqualTo("u206d25c2ea6bd87c17655609a1c37cb8");
166+
assertThat(messageEvent.timestamp()).isEqualTo(
167+
Instant.parse("2016-05-07T13:57:59.859Z").toEpochMilli());
168+
}
109169
}

spring-boot/line-bot-spring-boot-client/README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,14 @@ public class EchoApplication {
153153

154154
The Messaging API SDK is automatically configured by the system properties. The parameters are shown below.
155155

156-
| Parameter | Description |
157-
|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
158-
| line.bot.channel-token | Channel access token for the server |
159-
| line.bot.channel-secret | Channel secret for the server |
160-
| line.bot.channel-token-supply-mode | The way to fix channel access token. (default: `FIXED`)<br>LINE Partners should change this value to `SUPPLIER` and create custom `ChannelTokenSupplier` bean. |
161-
| line.bot.connect-timeout | Connection timeout in milliseconds |
162-
| line.bot.read-timeout | Read timeout in milliseconds |
163-
| line.bot.write-timeout | Write timeout in milliseconds |
164-
| line.bot.handler.enabled | Enable @EventMapping mechanism. (default: true) |
165-
| line.bot.handler.path | Path to waiting webhook. (default: `/callback`) |
156+
| Parameter | Description |
157+
|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
158+
| line.bot.channel-token | Channel access token for the server |
159+
| line.bot.channel-secret | Channel secret for the server |
160+
| line.bot.channel-token-supply-mode | The way to fix channel access token. (default: `FIXED`)<br>LINE Partners should change this value to `SUPPLIER` and create custom `ChannelTokenSupplier` bean. |
161+
| line.bot.connect-timeout | Connection timeout in milliseconds |
162+
| line.bot.read-timeout | Read timeout in milliseconds |
163+
| line.bot.write-timeout | Write timeout in milliseconds |
164+
| line.bot.skip-signature-verification | Whether to skip signature verification of webhooks. (default: false) |
165+
| line.bot.handler.enabled | Enable @EventMapping mechanism. (default: true) |
166+
| line.bot.handler.path | Path to waiting webhook. (default: `/callback`) |

spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/properties/LineBotProperties.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,13 @@ public record LineBotProperties(
8484
* Write timeout in milliseconds.
8585
*/
8686
@DefaultValue("10s")
87-
@Valid @NotNull Duration writeTimeout
87+
@Valid @NotNull Duration writeTimeout,
88+
89+
/*
90+
* Skip signature verification of webhooks.
91+
*/
92+
@DefaultValue("false")
93+
boolean skipSignatureVerification
8894
) {
8995
public enum ChannelTokenSupplyMode {
9096
/**

spring-boot/line-bot-spring-boot-client/src/test/java/com/linecorp/bot/spring/boot/core/properties/BotPropertiesValidatorTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ private LineBotProperties newLineBotProperties(
5555
URI.create("https://manager.line.biz/"),
5656
Duration.ofSeconds(10),
5757
Duration.ofSeconds(10),
58-
Duration.ofSeconds(10)
58+
Duration.ofSeconds(10),
59+
false
5960
);
6061
}
6162

spring-boot/line-bot-spring-boot-web/src/main/java/com/linecorp/bot/spring/boot/web/configuration/LineBotWebBeans.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818

1919
import java.nio.charset.StandardCharsets;
2020

21+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2122
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
2223
import org.springframework.context.annotation.Bean;
2324
import org.springframework.context.annotation.Import;
2425
import org.springframework.stereotype.Component;
2526

27+
import com.linecorp.bot.parser.FixedSkipSignatureVerificationSupplier;
2628
import com.linecorp.bot.parser.LineSignatureValidator;
29+
import com.linecorp.bot.parser.SkipSignatureVerificationSupplier;
2730
import com.linecorp.bot.parser.WebhookParser;
2831
import com.linecorp.bot.spring.boot.core.properties.LineBotProperties;
2932
import com.linecorp.bot.spring.boot.web.argument.support.LineBotDestinationArgumentProcessor;
@@ -41,6 +44,13 @@ public LineBotWebBeans(LineBotProperties lineBotProperties) {
4144
this.lineBotProperties = lineBotProperties;
4245
}
4346

47+
@Bean
48+
@ConditionalOnMissingBean(SkipSignatureVerificationSupplier.class)
49+
public SkipSignatureVerificationSupplier skipSignatureVerificationSupplier() {
50+
final boolean skipVerification = lineBotProperties.skipSignatureVerification();
51+
return FixedSkipSignatureVerificationSupplier.of(skipVerification);
52+
}
53+
4454
/**
4555
* Expose {@link LineSignatureValidator} as {@link Bean}.
4656
*/
@@ -55,7 +65,8 @@ public LineSignatureValidator lineSignatureValidator() {
5565
*/
5666
@Bean
5767
public WebhookParser lineBotCallbackRequestParser(
58-
LineSignatureValidator lineSignatureValidator) {
59-
return new WebhookParser(lineSignatureValidator);
68+
LineSignatureValidator lineSignatureValidator,
69+
SkipSignatureVerificationSupplier skipSignatureVerificationSupplier) {
70+
return new WebhookParser(lineSignatureValidator, skipSignatureVerificationSupplier);
6071
}
6172
}

0 commit comments

Comments
 (0)