Skip to content

Add option to skip signature verification #1635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>You can implement it to return whether to skip signature verification.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(add comment)
+ if true is passed, webhook signature verification is skipped. This may be helpful when you update channel secret and you want to skip the verification temporarily.

*
* <p>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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you update javadoc(comment) to explain when we should use SkipSignatureVerificationSupplier?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you create another constructor that doesn't break the build, and keep current constructor? Alternatively, you could set a non-skipping SkipSignatureVerificationSupplier as a default value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In cases other than temporary migration for channel secret, it should often be specified as true.

Having it set to return true by default in line-bot-sdk-java, with the possibility for users to override it if they wish, seems more convenient. It might not be necessary to require users to specify it, if we set default value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to say this?

In cases other than temporary migration for the channel secret, it should often be specified as false.

I have made the change to set the default to false. Please review it. b9c3430

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, yes

SkipSignatureVerificationSupplier skipSignatureVerificationSupplier) {
this.signatureValidator = requireNonNull(signatureValidator);
this.skipSignatureVerificationSupplier = requireNonNull(skipSignatureVerificationSupplier);
}

/**
Expand All @@ -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");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Event> 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<Event> 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());
}
}
21 changes: 11 additions & 10 deletions spring-boot/line-bot-spring-boot-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)<br>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`)<br>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`) |
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}.
*/
Expand All @@ -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);
}
}