diff --git a/line-bot-parser/src/main/java/com/linecorp/bot/parser/FixedSkipSignatureVerificationSupplier.java b/line-bot-parser/src/main/java/com/linecorp/bot/parser/FixedSkipSignatureVerificationSupplier.java new file mode 100644 index 000000000..d60ba6a1d --- /dev/null +++ b/line-bot-parser/src/main/java/com/linecorp/bot/parser/FixedSkipSignatureVerificationSupplier.java @@ -0,0 +1,34 @@ +/* + * Copyright 2025 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.bot.parser; + +public class FixedSkipSignatureVerificationSupplier implements SkipSignatureVerificationSupplier { + private final boolean fixedValue; + + public FixedSkipSignatureVerificationSupplier(boolean fixedValue) { + this.fixedValue = fixedValue; + } + + public static FixedSkipSignatureVerificationSupplier of(boolean fixedValue) { + return new FixedSkipSignatureVerificationSupplier(fixedValue); + } + + @Override + public boolean getAsBoolean() { + return fixedValue; + } +} diff --git a/line-bot-parser/src/main/java/com/linecorp/bot/parser/SkipSignatureVerificationSupplier.java b/line-bot-parser/src/main/java/com/linecorp/bot/parser/SkipSignatureVerificationSupplier.java new file mode 100644 index 000000000..47e9c15cb --- /dev/null +++ b/line-bot-parser/src/main/java/com/linecorp/bot/parser/SkipSignatureVerificationSupplier.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.bot.parser; + +import java.util.function.BooleanSupplier; + +/** + * Special {@link BooleanSupplier} for Skip Signature Verification. + * + *
You can implement it to return whether to skip signature verification. + * + *
If true is returned, webhook signature verification is skipped.
+ * This may be helpful when you update the channel secret and want to skip the verification temporarily.
+ */
+@FunctionalInterface
+public interface SkipSignatureVerificationSupplier extends BooleanSupplier {
+}
diff --git a/line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java b/line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java
index 52069eec3..87a36efb7 100644
--- a/line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java
+++ b/line-bot-parser/src/main/java/com/linecorp/bot/parser/WebhookParser.java
@@ -34,6 +34,7 @@ public class WebhookParser {
private final ObjectMapper objectMapper = ModelObjectMapper.createNewObjectMapper();
private final SignatureValidator signatureValidator;
+ private final SkipSignatureVerificationSupplier skipSignatureVerificationSupplier;
/**
* Creates a new instance.
@@ -42,6 +43,19 @@ public class WebhookParser {
*/
public WebhookParser(SignatureValidator signatureValidator) {
this.signatureValidator = requireNonNull(signatureValidator);
+ this.skipSignatureVerificationSupplier = FixedSkipSignatureVerificationSupplier.of(false);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param signatureValidator LINE messaging API's signature validator
+ * @param skipSignatureVerificationSupplier Supplier to determine whether to skip signature verification
+ */
+ public WebhookParser(SignatureValidator signatureValidator,
+ SkipSignatureVerificationSupplier skipSignatureVerificationSupplier) {
+ this.signatureValidator = requireNonNull(signatureValidator);
+ this.skipSignatureVerificationSupplier = requireNonNull(skipSignatureVerificationSupplier);
}
/**
@@ -62,7 +76,8 @@ public CallbackRequest handle(String signature, byte[] payload) throws IOExcepti
log.debug("got: {}", new String(payload, StandardCharsets.UTF_8));
}
- if (!signatureValidator.validateSignature(payload, signature)) {
+ if (!skipSignatureVerificationSupplier.getAsBoolean()
+ && !signatureValidator.validateSignature(payload, signature)) {
throw new WebhookParseException("Invalid API signature");
}
diff --git a/line-bot-parser/src/test/java/com/linecorp/bot/parser/WebhookParserTest.java b/line-bot-parser/src/test/java/com/linecorp/bot/parser/WebhookParserTest.java
index fbf69dc0d..69efe2b70 100644
--- a/line-bot-parser/src/test/java/com/linecorp/bot/parser/WebhookParserTest.java
+++ b/line-bot-parser/src/test/java/com/linecorp/bot/parser/WebhookParserTest.java
@@ -18,6 +18,8 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.InputStream;
@@ -52,7 +54,9 @@ public boolean validateSignature(byte[] content, String headerSignature) {
@BeforeEach
public void before() {
- parser = new WebhookParser(signatureValidator);
+ parser = new WebhookParser(
+ signatureValidator,
+ FixedSkipSignatureVerificationSupplier.of(false));
}
@Test
@@ -106,4 +110,60 @@ public void testCallRequest() throws Exception {
assertThat(messageEvent.timestamp()).isEqualTo(
Instant.parse("2016-05-07T13:57:59.859Z").toEpochMilli());
}
+
+ @Test
+ public void testSkipSignatureVerification() throws Exception {
+ final InputStream resource = getClass().getClassLoader().getResourceAsStream(
+ "callback-request.json");
+ final byte[] payload = resource.readAllBytes();
+
+ final var parser = new WebhookParser(
+ signatureValidator,
+ FixedSkipSignatureVerificationSupplier.of(true));
+
+ // assert no interaction with signatureValidator
+ verify(signatureValidator, never()).validateSignature(payload, "SSSSIGNATURE");
+
+ final CallbackRequest callbackRequest = parser.handle("SSSSIGNATURE", payload);
+
+ assertThat(callbackRequest).isNotNull();
+
+ final List
LINE Partners should change this value to `SUPPLIER` and create custom `ChannelTokenSupplier` bean. |
-| line.bot.connect-timeout | Connection timeout in milliseconds |
-| line.bot.read-timeout | Read timeout in milliseconds |
-| line.bot.write-timeout | Write timeout in milliseconds |
-| line.bot.handler.enabled | Enable @EventMapping mechanism. (default: true) |
-| line.bot.handler.path | Path to waiting webhook. (default: `/callback`) |
+| Parameter | Description |
+|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| line.bot.channel-token | Channel access token for the server |
+| line.bot.channel-secret | Channel secret for the server |
+| line.bot.channel-token-supply-mode | The way to fix channel access token. (default: `FIXED`)
LINE Partners should change this value to `SUPPLIER` and create custom `ChannelTokenSupplier` bean. |
+| line.bot.connect-timeout | Connection timeout in milliseconds |
+| line.bot.read-timeout | Read timeout in milliseconds |
+| line.bot.write-timeout | Write timeout in milliseconds |
+| line.bot.skip-signature-verification | Whether to skip signature verification of webhooks. (default: false) |
+| line.bot.handler.enabled | Enable @EventMapping mechanism. (default: true) |
+| line.bot.handler.path | Path to waiting webhook. (default: `/callback`) |
diff --git a/spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/properties/LineBotProperties.java b/spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/properties/LineBotProperties.java
index 28a84043e..b71bc87bc 100644
--- a/spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/properties/LineBotProperties.java
+++ b/spring-boot/line-bot-spring-boot-client/src/main/java/com/linecorp/bot/spring/boot/core/properties/LineBotProperties.java
@@ -84,7 +84,13 @@ public record LineBotProperties(
* Write timeout in milliseconds.
*/
@DefaultValue("10s")
- @Valid @NotNull Duration writeTimeout
+ @Valid @NotNull Duration writeTimeout,
+
+ /*
+ * Skip signature verification of webhooks.
+ */
+ @DefaultValue("false")
+ boolean skipSignatureVerification
) {
public enum ChannelTokenSupplyMode {
/**
diff --git a/spring-boot/line-bot-spring-boot-client/src/test/java/com/linecorp/bot/spring/boot/core/properties/BotPropertiesValidatorTest.java b/spring-boot/line-bot-spring-boot-client/src/test/java/com/linecorp/bot/spring/boot/core/properties/BotPropertiesValidatorTest.java
index 2af30ce66..498ed157e 100644
--- a/spring-boot/line-bot-spring-boot-client/src/test/java/com/linecorp/bot/spring/boot/core/properties/BotPropertiesValidatorTest.java
+++ b/spring-boot/line-bot-spring-boot-client/src/test/java/com/linecorp/bot/spring/boot/core/properties/BotPropertiesValidatorTest.java
@@ -55,7 +55,8 @@ private LineBotProperties newLineBotProperties(
URI.create("https://manager.line.biz/"),
Duration.ofSeconds(10),
Duration.ofSeconds(10),
- Duration.ofSeconds(10)
+ Duration.ofSeconds(10),
+ false
);
}
diff --git a/spring-boot/line-bot-spring-boot-web/src/main/java/com/linecorp/bot/spring/boot/web/configuration/LineBotWebBeans.java b/spring-boot/line-bot-spring-boot-web/src/main/java/com/linecorp/bot/spring/boot/web/configuration/LineBotWebBeans.java
index 6b5c6451c..bbd98b6fa 100644
--- a/spring-boot/line-bot-spring-boot-web/src/main/java/com/linecorp/bot/spring/boot/web/configuration/LineBotWebBeans.java
+++ b/spring-boot/line-bot-spring-boot-web/src/main/java/com/linecorp/bot/spring/boot/web/configuration/LineBotWebBeans.java
@@ -18,12 +18,15 @@
import java.nio.charset.StandardCharsets;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
+import com.linecorp.bot.parser.FixedSkipSignatureVerificationSupplier;
import com.linecorp.bot.parser.LineSignatureValidator;
+import com.linecorp.bot.parser.SkipSignatureVerificationSupplier;
import com.linecorp.bot.parser.WebhookParser;
import com.linecorp.bot.spring.boot.core.properties.LineBotProperties;
import com.linecorp.bot.spring.boot.web.argument.support.LineBotDestinationArgumentProcessor;
@@ -41,6 +44,13 @@ public LineBotWebBeans(LineBotProperties lineBotProperties) {
this.lineBotProperties = lineBotProperties;
}
+ @Bean
+ @ConditionalOnMissingBean(SkipSignatureVerificationSupplier.class)
+ public SkipSignatureVerificationSupplier skipSignatureVerificationSupplier() {
+ final boolean skipVerification = lineBotProperties.skipSignatureVerification();
+ return FixedSkipSignatureVerificationSupplier.of(skipVerification);
+ }
+
/**
* Expose {@link LineSignatureValidator} as {@link Bean}.
*/
@@ -55,7 +65,8 @@ public LineSignatureValidator lineSignatureValidator() {
*/
@Bean
public WebhookParser lineBotCallbackRequestParser(
- LineSignatureValidator lineSignatureValidator) {
- return new WebhookParser(lineSignatureValidator);
+ LineSignatureValidator lineSignatureValidator,
+ SkipSignatureVerificationSupplier skipSignatureVerificationSupplier) {
+ return new WebhookParser(lineSignatureValidator, skipSignatureVerificationSupplier);
}
}