Skip to content
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

Fix #689 by adding flags in AppConfig #690

Merged
merged 4 commits into from
Feb 16, 2021
Merged
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
42 changes: 24 additions & 18 deletions bolt/src/main/java/com/slack/api/bolt/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,36 +90,40 @@ public class App {
*/
private List<Middleware> middlewareList;

protected List<Middleware> buildDefaultMiddlewareList(AppConfig appConfig) {
protected List<Middleware> buildDefaultMiddlewareList(AppConfig config) {
List<Middleware> middlewareList = new ArrayList<>();

// ssl_check (slash command)
middlewareList.add(new SSLCheck(config().getVerificationToken()));
// ssl_check (slash command etc)
if (config.isSslCheckEnabled()) {
middlewareList.add(new SSLCheck(config.getVerificationToken()));
}

// request verification
// https://api.slack.com/docs/verifying-requests-from-slack
String signingSecret = appConfig.getSigningSecret();
if (signingSecret == null || signingSecret.trim().isEmpty()) {
// This is just a random value to avoid SlackSignature.Generator's initialization error.
// When this App runs through Socket Mode connections, it skips request signature verification.
signingSecret = "---";
if (config.isRequestVerificationEnabled()) {
// https://api.slack.com/docs/verifying-requests-from-slack
String signingSecret = config.getSigningSecret();
if (signingSecret == null || signingSecret.trim().isEmpty()) {
// This is just a random value to avoid SlackSignature.Generator's initialization error.
// When this App runs through Socket Mode connections, it skips request signature verification.
signingSecret = "---";
}
SlackSignature.Verifier verifier = new SlackSignature.Verifier(new SlackSignature.Generator(signingSecret));
RequestVerification requestVerification = new RequestVerification(verifier);
middlewareList.add(requestVerification);
}
SlackSignature.Verifier verifier = new SlackSignature.Verifier(new SlackSignature.Generator(signingSecret));
RequestVerification requestVerification = new RequestVerification(verifier);
middlewareList.add(requestVerification);

// single team authorization
if (appConfig.isDistributedApp()) {
middlewareList.add(new MultiTeamsAuthorization(config(), installationService));
} else if (appConfig.getSingleTeamBotToken() != null) {
if (config.isDistributedApp()) {
middlewareList.add(new MultiTeamsAuthorization(config, installationService));
} else if (config.getSingleTeamBotToken() != null) {
try {
AuthTestResponse initialAuthTest = client().authTest(r -> r.token(appConfig.getSingleTeamBotToken()));
AuthTestResponse initialAuthTest = client().authTest(r -> r.token(config.getSingleTeamBotToken()));
if (initialAuthTest == null || !initialAuthTest.isOk()) {
String error = initialAuthTest != null ? initialAuthTest.getError() : "";
String message = "The token is invalid (auth.test error: " + error + ")";
throw new IllegalArgumentException(message);
}
middlewareList.add(new SingleTeamAuthorization(config(), initialAuthTest, installationService));
middlewareList.add(new SingleTeamAuthorization(config, initialAuthTest, installationService));
} catch (IOException | SlackApiException e) {
String message = "The token is invalid (error: " + e.getMessage() + ")";
throw new IllegalArgumentException(message);
Expand All @@ -129,7 +133,9 @@ protected List<Middleware> buildDefaultMiddlewareList(AppConfig appConfig) {
}

// ignoring the events generated by this bot user
middlewareList.add(new IgnoringSelfEvents(appConfig.getSlack().getConfig()));
if (config.isIgnoringSelfEventsEnabled()) {
middlewareList.add(new IgnoringSelfEvents(config.getSlack().getConfig()));
}

return middlewareList;
}
Expand Down
15 changes: 15 additions & 0 deletions bolt/src/main/java/com/slack/api/bolt/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,19 @@ public void setOauthRedirectUriPath(String oauthRedirectUriPath) {
@Builder.Default
private boolean appInitializersEnabled = true;

// ---------------------------------
// Default middleware configuration
// ---------------------------------

// This middleware works only for HTTP Mode
@Builder.Default
private boolean sslCheckEnabled = true;

// This middleware works only for HTTP Mode
@Builder.Default
private boolean requestVerificationEnabled = true;

@Builder.Default
private boolean ignoringSelfEventsEnabled = true;

}
197 changes: 197 additions & 0 deletions bolt/src/test/java/test_locally/app/AppConfigTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package test_locally.app;

import com.slack.api.Slack;
import com.slack.api.SlackConfig;
import com.slack.api.app_backend.SlackSignature;
import com.slack.api.bolt.App;
import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.request.RequestHeaders;
import com.slack.api.bolt.request.builtin.BlockActionRequest;
import com.slack.api.bolt.request.builtin.EventRequest;
import com.slack.api.bolt.request.builtin.SSLCheckRequest;
import com.slack.api.bolt.response.Response;
import com.slack.api.model.event.ReactionAddedEvent;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import util.AuthTestMockServer;

import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.junit.Assert.*;

@Slf4j
public class AppConfigTest {

AuthTestMockServer server = new AuthTestMockServer();
SlackConfig config = new SlackConfig();
Slack slack = Slack.getInstance(config);

@Before
public void setup() throws Exception {
server.start();
config.setMethodsEndpointUrlPrefix(server.getMethodsEndpointPrefix());
}

@After
public void tearDown() throws Exception {
server.stop();
}

final String secret = "foo-bar-baz";
final SlackSignature.Generator generator = new SlackSignature.Generator(secret);

@Test
public void disableRequestVerification() throws Exception {
App app = new App(AppConfig.builder()
.signingSecret(secret)
.singleTeamBotToken(AuthTestMockServer.ValidToken)
.slack(slack)
.requestVerificationEnabled(false)
.build());
app.blockAction("a", (req, ctx) -> ctx.ack());


String payload = "{\n" +
" \"type\": \"block_actions\",\n" +
" \"user\": {\n" +
" \"id\": \"W111\",\n" +
" \"username\": \"primary-owner\",\n" +
" \"name\": \"primary-owner\",\n" +
" \"team_id\": \"T111\"\n" +
" },\n" +
" \"api_app_id\": \"A111\",\n" +
" \"token\": \"verification_token\",\n" +
" \"container\": {\n" +
" \"type\": \"message\",\n" +
" \"message_ts\": \"111.222\",\n" +
" \"channel_id\": \"C111\",\n" +
" \"is_ephemeral\": true\n" +
" },\n" +
" \"trigger_id\": \"111.222.valid\",\n" +
" \"team\": {\n" +
" \"id\": \"T111\",\n" +
" \"domain\": \"workspace-domain\",\n" +
" \"enterprise_id\": \"E111\",\n" +
" \"enterprise_name\": \"Sandbox Org\"\n" +
" },\n" +
" \"channel\": {\"id\": \"C111\", \"name\": \"test-channel\"},\n" +
" \"response_url\": \"https://hooks.slack.com/actions/T111/111/random-value\",\n" +
" \"actions\": [\n" +
" {\n" +
" \"action_id\": \"a\",\n" +
" \"block_id\": \"b\",\n" +
" \"text\": {\"type\": \"plain_text\", \"text\": \"Button\", \"emoji\": true},\n" +
" \"value\": \"click_me_123\",\n" +
" \"type\": \"button\",\n" +
" \"action_ts\": \"1596530385.194939\"\n" +
" }\n" +
" ]\n" +
"}";
String requestBody = "payload=" + URLEncoder.encode(payload, "UTF-8");
Map<String, List<String>> rawHeaders = new HashMap<>();
BlockActionRequest req = new BlockActionRequest(requestBody, payload, new RequestHeaders(rawHeaders));
Response response = app.run(req);
assertEquals(200L, response.getStatusCode().longValue());
}

@Test
public void disableSSLCheck() throws Exception {
String requestBody = "token=random&ssl_check=1";
Map<String, List<String>> rawHeaders = new HashMap<>();
SSLCheckRequest req = new SSLCheckRequest(requestBody, new RequestHeaders(rawHeaders));
{
App app = new App(AppConfig.builder()
.signingSecret(secret)
.singleTeamBotToken(AuthTestMockServer.ValidToken)
.slack(slack)
.build());
Response response = app.run(req);
assertEquals(200, response.getStatusCode().longValue());
}
{
App app = new App(AppConfig.builder()
.signingSecret(secret)
.singleTeamBotToken(AuthTestMockServer.ValidToken)
.slack(slack)
.sslCheckEnabled(false) // modified
.build());
Response response = app.run(req);
assertEquals(404L, response.getStatusCode().longValue());
}
}

void setRequestHeaders(String requestBody, Map<String, List<String>> rawHeaders, String timestamp) {
rawHeaders.put(SlackSignature.HeaderNames.X_SLACK_REQUEST_TIMESTAMP, Arrays.asList(timestamp));
rawHeaders.put(SlackSignature.HeaderNames.X_SLACK_SIGNATURE, Arrays.asList(generator.generate(timestamp, requestBody)));
}

@Test
public void disableIgnoringSelfEvents() throws Exception {
String json = "{\n" +
" \"token\": \"verification_token\",\n" +
" \"team_id\": \"T111\",\n" +
" \"enterprise_id\": \"E111\",\n" +
" \"api_app_id\": \"A111\",\n" +
" \"event\": {\n" +
" \"type\": \"reaction_added\",\n" +
" \"user\": \"U1234567\",\n" + // This value needs to be the same with auth.test API response
" \"item\": {\n" +
" \"type\": \"message\",\n" +
" \"channel\": \"C111\",\n" +
" \"ts\": \"1599529504.000400\"\n" +
" },\n" +
" \"reaction\": \"heart_eyes\",\n" +
" \"item_user\": \"W111\",\n" +
" \"event_ts\": \"1599616881.000800\"\n" +
" },\n" +
" \"type\": \"event_callback\",\n" +
" \"event_id\": \"Ev111\",\n" +
" \"event_time\": 1599616881\n" +
"}";
Map<String, List<String>> rawHeaders = new HashMap<>();
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
setRequestHeaders(json, rawHeaders, timestamp);

{
App app = new App(AppConfig.builder()
.signingSecret(secret)
.singleTeamBotToken(AuthTestMockServer.ValidToken)
.slack(slack)
.build());
final AtomicBoolean called = new AtomicBoolean(false);
app.event(ReactionAddedEvent.class, (req, ctx) -> {
called.set(true);
return ctx.ack();
});
EventRequest req = new EventRequest(json, new RequestHeaders(rawHeaders));
Response response = app.run(req);
assertEquals(200L, response.getStatusCode().longValue());
assertFalse(called.get());
}
{
App app = new App(AppConfig.builder()
.signingSecret(secret)
.singleTeamBotToken(AuthTestMockServer.ValidToken)
.slack(slack)
.ignoringSelfEventsEnabled(false) // modified
.build());
final AtomicBoolean called = new AtomicBoolean(false);
app.event(ReactionAddedEvent.class, (req, ctx) -> {
called.set(true);
return ctx.ack();
});
EventRequest req = new EventRequest(json, new RequestHeaders(rawHeaders));
Response response = app.run(req);
assertEquals(200L, response.getStatusCode().longValue());
assertTrue(called.get());
}
}

}
25 changes: 25 additions & 0 deletions docs/guides/bolt-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,31 @@ A set of the built-in middleware precedes your custom middleware. So, if the app

The most common would be the case where a request has been denied by **RequestVerification** middleware. After the denial, any middleware won't be executed, so that the above middleware also doesn't work for the case.

#### Customize the Built-in Middleware List

Bolt turns the following middleware on by default:

* [RequestVerification](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/RequestVerification.java) to verify the signature in HTTP Mode requests
* [SingleTeamAuthorization](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/SingleTeamAuthorization.java) or [MultiTeamsAuthorization](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/MultiTeamsAuthorization.java) to lookup the valid OAuth access token for handling a request
* [IgnoringSelfEvents](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/IgnoringSelfEvents.java) to skip the events generated by this app's bot user (this is useful for avoiding code error causing an infinite loop)
* [SSLCheck](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/SSLCheck.java) to handle `ssl_check=1` requests from Slack

Although we generally do not recommend disabling these middleware as they are commonly necessary, you can disable them using the flags like `ignoringSelfEventsEnabled` in `AppConfig` objects.

```java
AppConfig appConfig = new AppConfig();

appConfig.setIgnoringSelfEvents(false); // the default is true
appConfig.setSslCheck(false); // the default is true

// Please don't do this without an alternative solution
appConfig.setRequestVerification(false); // the default is true

App app = new App(appConfig);
```

Make sure if it's safe enough when you turn a built-in middleware off. **We strongly recommend using `RequestVerification` for better security**. If you have a proxy that verifies request signature in front of the Bolt app, it's totally fine to disable `RequestVerification` to avoid duplication of work. Don't turn it off just for easiness of development.

---
## Supported Web Frameworks

Expand Down
25 changes: 25 additions & 0 deletions docs/guides/ja/bolt-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,31 @@ Bolt に標準で組み込まれているミドルウェアはアプリ側で追

最もよくあるパターンは **RequestVerification** ミドルウェアでリクエストが拒否される場合です。この拒否のあとは、他のどのミドルウェアも実行されないため、上記のサンプル例のミドルウェアも同様に動作しません。

#### 組み込みミドルウェアをカスタマイズする

Bolt は、デフォルトで以下のミドルウェアを有効にします。

* [RequestVerification](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/RequestVerification.java) は HTTP モードでリクエストの署名を検証します
* [SingleTeamAuthorization](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/SingleTeamAuthorization.java) または [MultiTeamsAuthorization](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/MultiTeamsAuthorization.java) はリクエストに対応する OAuth アクセストークンをルックアップします
* [IgnoringSelfEvents](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/IgnoringSelfEvents.java) はそのアプリの bot user が発生させたイベントをスキップします(これは無限ループを起こすようなコーディングミスを防止するために有用です)
* [SSLCheck](https://github.com/slackapi/java-slack-sdk/blob/main/bolt/src/main/java/com/slack/api/bolt/middleware/builtin/SSLCheck.java) は Slack からの `ssl_check=1` リクエストをハンドリングします

一般的にこれらのミドルウェアは共通で必要となるもなので無効化することを推奨しませんが、`AppConfig` オブジェクトの `ignoringSelfEventsEnabled` などのフラグを設定して無効化することができます。

```java
AppConfig appConfig = new AppConfig();

appConfig.setIgnoringSelfEvents(false); // デフォルトは true
appConfig.setSslCheck(false); // デフォルトは true

// 代替となるソリューションなしで無効化しないでください!
appConfig.setRequestVerification(false); // デフォルトは true

App app = new App(appConfig);
```

組み込みのミドルウェアを無効化するときはそれが十分安全か確認するようにしてください。**特に、よりよいアプリのセキュリティを確保するために `RequestVerification` を使うことを強く推奨しています**。もし Bolt アプリの前段にリクエストの署名を検証してくれるプロキシサーバーを持っているなら、 `RequestVerification` を無効化することは全く問題ないでしょう。しかし、ただ開発が簡単になるからという理由だけで、このミドルウェアを無効化しないでください。

---
## 対応している Web フレームワーク

Expand Down