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 result = callbackRequest.events(); + + @SuppressWarnings("rawtypes") + final MessageEvent messageEvent = (MessageEvent) result.get(0); + final TextMessageContent text = (TextMessageContent) messageEvent.message(); + assertThat(text.text()).isEqualTo("Hello, world"); + + final String followedUserId = messageEvent.source().userId(); + assertThat(followedUserId).isEqualTo("u206d25c2ea6bd87c17655609a1c37cb8"); + assertThat(messageEvent.timestamp()).isEqualTo( + Instant.parse("2016-05-07T13:57:59.859Z").toEpochMilli()); + } + + @Test + public void testWithoutSkipSignatureVerificationSupplierInConstructor() throws Exception { + final InputStream resource = getClass().getClassLoader().getResourceAsStream( + "callback-request.json"); + final byte[] payload = resource.readAllBytes(); + + when(signatureValidator.validateSignature(payload, "SSSSIGNATURE")).thenReturn(true); + + final var parser = new WebhookParser(signatureValidator); + final CallbackRequest callbackRequest = parser.handle("SSSSIGNATURE", payload); + + assertThat(callbackRequest).isNotNull(); + + final List result = callbackRequest.events(); + + @SuppressWarnings("rawtypes") + final MessageEvent messageEvent = (MessageEvent) result.get(0); + final TextMessageContent text = (TextMessageContent) messageEvent.message(); + assertThat(text.text()).isEqualTo("Hello, world"); + + final String followedUserId = messageEvent.source().userId(); + assertThat(followedUserId).isEqualTo("u206d25c2ea6bd87c17655609a1c37cb8"); + assertThat(messageEvent.timestamp()).isEqualTo( + Instant.parse("2016-05-07T13:57:59.859Z").toEpochMilli()); + } } diff --git a/spring-boot/line-bot-spring-boot-client/README.md b/spring-boot/line-bot-spring-boot-client/README.md index 8747914db..e6e850331 100644 --- a/spring-boot/line-bot-spring-boot-client/README.md +++ b/spring-boot/line-bot-spring-boot-client/README.md @@ -153,13 +153,14 @@ public class EchoApplication { The Messaging API SDK is automatically configured by the system properties. The parameters are shown below. -| 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.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); } }