Slack SDK for Java

App Distribution (OAuth)

A newly created Slack app can only be installed in its development workspace in the beginning. By setting an OAuth Redirect URL and enabling App Distribution, the app becomes to be ready for installation in any other workspaces.

Slack App Configuration

To enable App Distribution, visit the Slack App configuration page, choose the app you’re working on, go to Settings > Manage Distribution on the left pane, and follow the instructions there.

For Redirect URL, Bolt apps respond to https://{your app's public URL domain}/slack/oauth/callback if you go with recommended settings. To know how to configure such settings, consult the list of the available env variables below in this page.

Bolt for Java automatically includes support for org wide installations since version 1.4.0. Org wide installations can be enabled in your app configuration settings under Org Level Apps.

What Your Bolt App Does

All your app needs to do to properly handle OAuth Flow are:

  • Provide an endpoint starting OAuth flow by redirecting installers to Slack’s Authorize endpoint with sufficient parameters
    • Generate a state parameter value to verify afterwards
    • Append client_id, scope, user_scope (only for v2), and state to the URL
  • Provide an endpoint to handle user redirection from Slack
    • Make sure if the state parameter is valid
    • Complete the installation by calling oauth.v2.access (or oauth.access if you maintain legacy OAuth apps) method and store the acquired tokens
  • Provide the endpoints to navigate installers for the completion/cancellation of the installation flow
    • The URLs are usually somewhere else but Bolt has simple functionality to serve them

Examples

Here is a Bolt app demonstrating how to implement OAuth flow. As the OAuth flow handling features are unnecessary for many custom apps, those are disabled by default. App instances need to explicitly call asOAuthApp(true) to turn on them.

import com.slack.api.bolt.App;
import com.slack.api.bolt.jetty.SlackAppServer;
import java.util.HashMap;
import java.util.Map;
import static java.util.Map.entry;

// API Request Handler App
//  expected env variables:
//   SLACK_SIGNING_SECRET
App apiApp = new App();
apiApp.command("/hi", (req, ctx) -> {
  return ctx.ack("Hi there!");
});

// OAuth Flow Handler App
//  expected env variables:
//   SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_REDIRECT_URI, SLACK_SCOPES,
//   SLACK_INSTALL_PATH, SLACK_REDIRECT_URI_PATH
//   SLACK_OAUTH_COMPLETION_URL, SLACK_OAUTH_CANCELLATION_URL
App oauthApp = new App().asOAuthApp(true);

// Mount the two apps with their root path
SlackAppServer server = new SlackAppServer(new HashMap<>(Map.ofEntries(
  entry("/slack/events", apiApp), // POST /slack/events (incoming API requests from the Slack Platform)
  entry("/slack/oauth", oauthApp) // GET  /slack/oauth/start, /slack/oauth/callback (user access)
)));

server.start(); // http://localhost:3000

Technically, it’s possible to use a single App for both Slack API requests and direct user interactions for the OAuth flow. But most apps probably will prefer to have a different root path for OAuth interactions.

Slack Config for Distributing Your Slack App

Here is the list of the necessary configurations for distributing apps built with Bolt. If you prefer using other env variable names or other solutions to load this information, implement your own way to load AppConfig instead.

Env Variable Name Description (Where to find the value)
SLACK_SIGNING_SECRET Signing Secret: A secret key for verifying requests from Slack. (Find at Settings > Basic Information > App Credentials)
SLACK_CLIENT_ID OAuth 2.0 Client ID (Find at Settings > Basic Information > App Credentials)
SLACK_CLIENT_SECRET OAuth 2.0 Client Secret (Find at Settings > Basic Information > App Credentials)
SLACK_REDIRECT_URI OAuth 2.0 Redirect URI (Configure at Features > OAuth & Permissions > Redirect URLs)
SLACK_SCOPES Command-separated list of scopes: scope parameter that will be appended to https://slack.com/oauth/authorize and https://slack.com/oauth/v2/authorize as a query parameter (Find at Settings > Manage Distribution > Sharable URL, extract the value for scope)
SLACK_USER_SCOPES (only for v2) Command-separated list of user scopes: user_scope parameter that will be appended to https://slack.com/oauth/v2/authorize as a query parameter (Find at Settings > Manage Distribution > Sharable URL, extract the value for user_scope)
SLACK_INSTALL_PATH Starting point of OAuth flow: This endpoint redirects users to the Slack Authorize endpoint with required query parameters such as client_id, scope, user_scope (only for v2), and state.

The suggested path is /slack/oauth/start but you can go with any path. Note that the example above automatically prepends /slack/oauth to this variable.
SLACK_REDIRECT_URI_PATH Path for OAuth Redirect URI: This endpoint handles callback requests after the Slack’s OAuth confirmation. The path must be consistent with SLACK_REDIRECT_URI value.

The suggested path is /slack/oauth/callback but you can go with any path. Note that the example above automatically prepends /slack/oauth to this variable.
SLACK_OAUTH_COMPLETION_URL Installation Completion URL: The complete public URL to redirect users when their installations have been successfully completed. You can go with any URLs.
SLACK_OAUTH_CANCELLATION_URL Installation Cancellation/Error URL: The complete public URL to redirect users when their installations have been cancelled for some reasons. You can go with any URLs.

Choose Proper Storage Services

By default, OAuth flow supported Bolt apps uses the local file system to generate/store state parameters, and store bot/user tokens. Bolt supports the following out-of-the-box.

  • Local File System
  • Amazon S3
  • Relational Database (via JDBC) - coming soon!

If your datastore is unsupported, you can implement the interfaces com.slack.api.bolt.service.InstallationService and com.slack.api.bolt.service.OAuthStateService on your own.

Here is an example app demonstrating how to enable Amazon S3 backed services.

import com.slack.api.bolt.App;
import com.slack.api.bolt.jetty.SlackAppServer;
import com.slack.api.bolt.service.InstallationService;
import com.slack.api.bolt.service.OAuthStateService;
import com.slack.api.bolt.service.builtin.AmazonS3InstallationService;
import com.slack.api.bolt.service.builtin.AmazonS3OAuthStateService;

import java.util.HashMap;
import java.util.Map;
import static java.util.Map.entry;

// The standard AWS env variables are expected
// export AWS_REGION=us-east-1
// export AWS_ACCESS_KEY_ID=AAAA*************
// export AWS_SECRET_ACCESS_KEY=4o7***********************

// Please be careful about the security policies on this bucket.
String awsS3BucketName = "YOUR_OWN_BUCKET_NAME_HERE";

InstallationService installationService = new AmazonS3InstallationService(awsS3BucketName);
// Set true if you'd like to store every single installation as a different record
installationService.setHistoricalDataEnabled(true);

// apiApp uses only InstallationService to access stored tokens
App apiApp = new App();
apiApp.command("/hi", (req, ctx) -> {
  return ctx.ack("Hi there!");
});
apiApp.service(installationService);

// Needless to say, oauthApp uses InstallationService
// In addition, it uses OAuthStateService to create/read/delete state parameters
App oauthApp = new App().asOAuthApp(true);
oauthApp.service(installationService);

// Store valid state parameter values in Amazon S3 storage
OAuthStateService stateService = new AmazonS3OAuthStateService(awsS3BucketName);
// This service is necessary only for OAuth flow apps
oauthApp.service(stateService);

// Mount the two apps with their root path
SlackAppServer server = new SlackAppServer(new HashMap<>(Map.ofEntries(
  entry("/slack/events", apiApp), // POST /slack/events (incoming API requests from the Slack Platform)
  entry("/slack/oauth", oauthApp) // GET  /slack/oauth/start, /slack/oauth/callback (user access)
)));

server.start(); // http://localhost:3000

If you want to turn the token rotation feature on, your InstallationService should be compatible with it. Refer to the v1.9.0 release notes for more details.

Granular Permission Apps or Classic Apps

Slack has two types of OAuth flows for Slack app installations. The V2 (this is a bit confusing but it’s not the version of OAuth spec, but the version of the Slack OAuth flow) OAuth flow enables Slack apps to request more granular permissions than the classic ones, especially for bot users. The differences between the two types are having v2 in the endpoint to issue access tokens and the OAuth Authorization URL, plus some changes to the response data structure returned by the oauth(.v2).access endpoint.

V2 OAuth 2.0 Flow (default)

Authorization URL https://slack.com/oauth/v2/authorize
Web API to issue access tokens oauth.v2.access (Response)

Classic OAuth Flow

Authorization URL https://slack.com/oauth/authorize
Web API to issue access tokens oauth.access (Response)

By default, Bolt enables the V2 OAuth Flow over the classic one. It’s configurable by AppConfig’s the setter method for classicAppPermissionsEnabled. The value is set to false by default. Change the flag to true to authorize your classic OAuth apps.

AppConfig appConfig = new AppConfig();
appConfig.setClassicAppPermissionsEnabled(true);
App app = new App(appConfig);

InstallationService absorbs the difference in the response structure. So, you don’t need to change anything even when you switch from the classic OAuth to the V2.

Build Slack OAuth using Spring Boot

Implementing Slack OAuth flow app using Spring Boot is quite easy. All you need to do are to 1) load env variables, 2) to initialize App with services and listeners as a Spring Bean, and 3) to have three endpoints to handle HTTP requests. Please note that Bolt properly works with Spring Boot 2.2 or newer versions.

package hello;

// export SLACK_SIGNING_SECRET=xxx
// export SLACK_CLIENT_ID=111.222
// export SLACK_CLIENT_SECRET=xxx
// export SLACK_SCOPES=commands,chat:write.public,chat:write
// export SLACK_USER_SCOPES=
// export SLACK_INSTALL_PATH=/slack/install
// export SLACK_REDIRECT_URI_PATH=/slack/oauth_redirect
// export SLACK_OAUTH_COMPLETION_URL=https://www.example.com/completion
// export SLACK_OAUTH_CANCELLATION_URL=https://www.example.com/cancellation

import com.slack.api.bolt.App;
import javax.servlet.annotation.WebServlet;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SlackApp {
  @Bean
  public App initSlackApp() {
    App app = new App().asOAuthApp(true); // Do not forget calling `asOAuthApp(true)` here
    app.command("/hello-oauth-app", (req, ctx) -> {
      return ctx.ack("What's up?");
    });
    return app;
  }
}

import com.slack.api.bolt.servlet.SlackAppServlet;
import com.slack.api.bolt.servlet.SlackOAuthAppServlet;

@WebServlet("/slack/events")
public class SlackEventsController extends SlackAppServlet {
  public SlackEventsController(App app) { super(app); }
}
@WebServlet("/slack/install")
public class SlackOAuthInstallController extends SlackOAuthAppServlet {
  public SlackOAuthInstallController(App app) { super(app); }
}
@WebServlet("/slack/oauth_redirect")
public class SlackOAuthRedirectController extends SlackOAuthAppServlet {
  public SlackOAuthRedirectController(App app) { super(app); }
}

If you want to use different implementations of InstallationService and OAuthStateService, you can have them as Spring components this way:

public class SlackApp {
  // Please be careful about the security policies on this bucket.
  private static final String S3_BUCKET_NAME = "your-s3-bucket-name";

  @Bean
  public InstallationService initInstallationService() {
    InstallationService installationService = new AmazonS3InstallationService(S3_BUCKET_NAME);
    installationService.setHistoricalDataEnabled(true);
    return installationService;
  }

  @Bean
  public OAuthStateService initStateService() {
    return new AmazonS3OAuthStateService(S3_BUCKET_NAME);
  }

  @Bean
  public App initSlackApp(InstallationService installationService, OAuthStateService stateService) {
    App app = new App().asOAuthApp(true);
    app.service(installationService);
    app.service(stateService);
    return app;
  }
}

Use the Built-in tokens_revoked / app_uninstalled Event Handlers

For secure data management for your customers and end-users, properly handling tokens_revoked and app_uninstalled events is crucial. Bolt for Java provides the built-in event handlers for these events, which seamlessly integrated with your InstallationService’s deletion methods.

App app = new App();
InstallationService installationService = new MyInstallationService();
app.service(installationService);
// Turn the event handlers on
app.enableTokenRevocationHandlers();

The above code is equivalent to the following:

App app = new App();
InstallationService installationService = new MyInstallationService();
app.service(installationService);
// Turn the event handlers on
app.event(TokensRevokedEvent.class, app.defaultTokensRevokedEventHandler());
app.event(AppUninstalledEvent.class, app.defaultAppUninstalledEventHandler());

To enable your own custom InstallationService classes to work with the built-in event handlers, the classes need to implement the following methods in the InstallationService interface:

  • void deleteBot(Bot bot)
  • void deleteInstaller(Installer installer)
  • void deleteAll(String enterpriseId, String teamId)

Serve the Completion/Cancellation Pages in Bolt Apps

Although most apps tend to choose static pages for the completion/cancellation URLs, it’s also possible to dynamically serve those URLs in the same app. Bolt doesn’t offer any features to render web pages. Use your favorite template engine for it.

String renderCompletionPageHtml(String queryString) { return null; }
String renderCancellationPageHtml(String queryString) { return null; }

oauthApp.endpoint("GET", "/slack/oauth/completion", (req, ctx) -> {
  return Response.builder()
    .statusCode(200)
    .contentType("text/html")
    .body(renderCompletionPageHtml(req.getQueryString()))
    .build();
});

oauthApp.endpoint("GET", "/slack/oauth/cancellation", (req, ctx) -> {
  return Response.builder()
    .statusCode(200)
    .contentType("text/html")
    .body(renderCancellationPageHtml(req.getQueryString()))
    .build();
});