diff --git a/docs/_includes/sidebar-ja.md b/docs/_includes/sidebar-ja.md index 6bb1d05c6..3de092482 100644 --- a/docs/_includes/sidebar-ja.md +++ b/docs/_includes/sidebar-ja.md @@ -22,6 +22,7 @@
  • イベント API
  • ワークフローステップ
  • アプリの配布 (OAuth)
  • +
  • Slack でログインする
  • 対応 Web フレームワーク
  • diff --git a/docs/_includes/sidebar.md b/docs/_includes/sidebar.md index 5a2046fb6..62d5d7f0e 100644 --- a/docs/_includes/sidebar.md +++ b/docs/_includes/sidebar.md @@ -22,6 +22,7 @@
  • Events API
  • Workflow Steps
  • App Distribution (OAuth)
  • +
  • Sign in with Slack
  • Supported Web Frameworks
  • diff --git a/docs/guides/app-distribution.md b/docs/guides/app-distribution.md index a031e12d7..8549c07e0 100644 --- a/docs/guides/app-distribution.md +++ b/docs/guides/app-distribution.md @@ -71,7 +71,7 @@ Technically, it's possible to use a single **App** for both Slack API requests a ### 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 **SlackConfig** instead. +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)| |-|-| @@ -147,6 +147,8 @@ SlackAppServer server = new SlackAppServer(Map.of( server.start(); // http://localhost:3000 ``` +If you want to turn [the token rotation feature](https://api.slack.com/authentication/rotation) on, your `InstallationService` should be compatible with it. Refer to the [v1.9.0 release notes](https://github.com/slackapi/java-slack-sdk/releases/tag/v1.9.0) 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. diff --git a/docs/guides/ja/app-distribution.md b/docs/guides/ja/app-distribution.md index 106da6bf6..7da3a51b3 100644 --- a/docs/guides/ja/app-distribution.md +++ b/docs/guides/ja/app-distribution.md @@ -70,7 +70,7 @@ server.start(); // http://localhost:3000 ### 配布可能な Slack アプリのための設定 -以下は配布可能なアプリのための設定項目の一覧です。もしこれら以外の環境変数名や、別の読み込みの仕組みを使いたい場合は、自前で **SlackConfig** を初期化する実装を行ってください。 +以下は配布可能なアプリのための設定項目の一覧です。もしこれら以外の環境変数名や、別の読み込みの仕組みを使いたい場合は、自前で **AppConfig** を初期化する実装を行ってください。 |環境変数名|説明 (値を見つけられる場所)| |-|-| @@ -80,7 +80,7 @@ server.start(); // http://localhost:3000 |**SLACK_REDIRECT_URI**|**OAUth 2.0 Redirect URI** (**Features** > **OAuth & Permissions** > **Redirect URLs**)| |**SLACK_SCOPES**|**カンマ区切りの bot scope リスト**: `scope` パラメーターは `https://slack.com/oauth/authorize` や `https://slack.com/oauth/v2/authorize` にクエリパラメーターとして付加されます (**Settings** > **Manage Distribution** > **Sharable URL** から `scope` の値を取得)| |**SLACK_USER_SCOPES** (v2 のみ)|**カンマ区切りの user scope リスト**: `user_scope` パラメーターは `https://slack.com/oauth/v2/authorize` にクエリパラメーターとして付加されます (**Settings** > **Manage Distribution** > **Sharable URL**, から `user_scope` の値を取得)| -|**SLACK_INSTALL_PATH**|**OAuth フローの開始点**: このエンドポイントはユーザーを `client_id`, `scope`, `user_scope` (v2 のみ), and `state` とともに Slack の Authorize エンドポイントにリダイレクトします。推奨するパスは `/slack/oauth/start` ですが、どのようなパスでも構いません。| +|**SLACK_INSTALL_PATH**|**OAuth フローの開始点**: このエンドポイントはユーザーを `client_id`, `scope`, `user_scope` (v2 のみ), `state` とともに Slack の Authorize エンドポイントにリダイレクトします。推奨するパスは `/slack/oauth/start` ですが、どのようなパスでも構いません。| |**SLACK_REDIRECT_URI_PATH**|**OAuth Redirect URI**: このエンドポイントは Slack の OAuth 許可確認画面からの callback リクエストを処理します。このパスは **SLACK_REDIRECT_URI** の値と整合している必要があります。推奨のパスは `/slack/oauth/callback` ですが、どのようなパスでも構いません。| |**SLACK_OAUTH_COMPLETION_URL**|**Installation Completion URL**: インストール完了画面の URL を指定します。どんな URL でも構いません。| |**SLACK_OAUTH_CANCELLATION_URL**|**Installation Cancellation/Error URL**: キャンセルやエラーが発生したときの遷移先 URL を指定します。どんな URL でも構いません。| @@ -146,6 +146,8 @@ SlackAppServer server = new SlackAppServer(Map.of( server.start(); // http://localhost:3000 ``` +もし[トークンローテーション](https://api.slack.com/authentication/rotation)を有効にしたいという場合は、あなたの `InstallationService` がトークンローテーション互換である必要があります。詳細は [v1.9.0 のリリースノート(英語)](https://github.com/slackapi/java-slack-sdk/releases/tag/v1.9.0)を参考にしてください。 + ### Granular Permission Apps と Classic Apps Slack アプリインストールには、二つの OAuth フローがあります。V2(ちょっと紛らわしいですが OAuth のバージョンではなく Slack OAuth フローのバージョンです)の OAuth フローでの Slack アプリは(特にボットユーザーの権限に関して)旧来に比べてより詳細な必要最小限の権限だけをリクエストできるようになりました。二つのやり方の違いは `v2` を Authorization URL やトークンを発行する API メソッドの URL に含んでいることと、API レスポンスのデータ構造に若干の変更が加わっていることです。 diff --git a/docs/guides/ja/sign-in-with-slack.md b/docs/guides/ja/sign-in-with-slack.md new file mode 100644 index 000000000..eb6f204d0 --- /dev/null +++ b/docs/guides/ja/sign-in-with-slack.md @@ -0,0 +1,146 @@ +--- +layout: ja +title: "Slack でログインする (OpenID Connect)" +lang: ja +--- + +# Slack でログインする (OpenID Connect) + +[Slack でログインする (Sign in with Slack)](https://api.slack.com/authentication/sign-in-with-slack)という機能は、ユーザーが Slack アカウントを使って他のサービスにログインすることに役立ちます。このプラットフォーム機能は、標準の [OpenID Connect](https://openid.net/connect/) の仕様と互換性を持つように最近アップグレードされました。Bolt for Java の 1.10 以上のバージョンであれば、この認証フローを非常に簡単に実装することができます。 + +### Slack アプリの設定 + +新しい Slack アプリをつくるときに、以下のユーザースコープを設定してください: + +```yaml +oauth_config: + redirect_urls: + - https://example.com/replace-this-with-your-own-redirect-uri + scopes: + user: + - openid # 必須 + - email # オプショナル + - profile # オプショナル +``` + + +### OpenID Connect アプリのための設定 + +以下は OpenID Connect 互換のアプリのための設定項目の一覧です。もしこれら以外の環境変数名や、別の読み込みの仕組みを使いたい場合は、自前で **AppConfig** を初期化する実装を行ってください。 + +|環境変数名|説明 (値を見つけられる場所)| +|-|-| +|**SLACK_CLIENT_ID**|**Client ID** (Find at **Settings** > **Basic Information** > **App Credentials**)| +|**SLACK_CLIENT_SECRET**|**Client Secret** (Find at **Settings** > **Basic Information** > **App Credentials**)| +|**SLACK_REDIRECT_URI**|**Redirect URI** (Configure at **Features** > **OAuth & Permissions** > **Redirect URLs**)| +|**SLACK_USER_SCOPES**|**カンマ区切りの user scope リスト**: `scope` パラメーターは `https://slack.com/openid/connect/authorize` にクエリパラメーターとして付加されます。可能な値は `openid`, `email`, `profile` です。| +|**SLACK_INSTALL_PATH**|**OpenID Connect フローの開始点**: このエンドポイントはユーザーを `client_id`, `scope`, `state`, `nonce` (オプショナル) のクエリパラメーターとともに Slack の OpenID Connect エンドポイントにリダイレクトします。| +|**SLACK_REDIRECT_URI_PATH**|**OpenID Connect Redirect URI**: このエンドポイントは Slack の OAuth 許可確認画面からの callback リクエストを処理します。このパスは **SLACK_REDIRECT_URI** の値と整合している必要があります。| + +### コード例 + +どのようにエンドユーザーの OpenID Connect のフローをハンドリングするかを知るには、[Servlet アプリの例](https://github.com/slackapi/java-slack-sdk/blob/main/bolt-servlet/src/test/java/samples/OpenIDConnectSample.java)を参考にしてみてください。 + +```java +import java.util.*; + +// implementation 'com.slack.api:bolt-jetty:{the latest version}' +import com.slack.api.Slack; +import com.slack.api.bolt.App; +import com.slack.api.bolt.jetty.SlackAppServer; + +// id_token の JWT の値をでコードしたい場合は、以下の外部ライブラリを使う: +// implementation 'com.auth0:java-jwt:{the latest version}' +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; + +// 以下の環境変数が設定されていることが前提: +// SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_REDIRECT_URI, SLACK_USER_SCOPES +App app = new App().asOpenIDConnectApp(true); + +// このコールバック関数で OpenID Connect の code authorization フローをハンドリングできる +app.openIDConnectSuccess((req, resp, token) -> { + var logger = req.getContext().getLogger(); + + // TODO: 渡された "token" レスポンス (openid.connect.token API 応答) を保存 + + // openid.connect.token レスポンスの id_token をデコード + DecodedJWT decoded = JWT.decode(token.getIdToken()); + Map claims = decoded.getClaims(); + logger.info("claims: {}", claims); + + var teamId = claims.get("https://slack.com/team_id").asString(); + + // 取得したアクセストークンで openid.connect.userInfo API を呼び出す実装例 + var client = Slack.getInstance().methods(); + try { + var userInfo = client.openIDConnectUserInfo(r -> r.token(token.getAccessToken())); + logger.info("userInfo: {}", userInfo); + + } catch (Exception e) { + throw new RuntimeException(e); + } + + // エンドユーザーにWebページを表示(または、他のサービスとの OAuth フローに続けるなどどこかにリダイレクトしてもよい) + var html = app.config().getOAuthRedirectUriPageRenderer().renderSuccessPage( + null, req.getContext().getOauthCompletionUrl()); + resp.setBody(html); + resp.setContentType("text/html; charset=utf-8"); + return resp; +}); + +Map apps = new HashMap<>(); +apps.put("/slack/", app); +SlackAppServer server = new SlackAppServer(apps); +server.start(); +``` + +もし、[トークンローテーション(英語)](https://api.slack.com/authentication/rotation)の機能も同時に有効にする場合、コードは以下のようになるでしょう: + +```java +// このコールバック関数で OpenID Connect の code authorization フローをハンドリングできる +app.openIDConnectSuccess((req, resp, token) -> { + var logger = req.getContext().getLogger(); + + // TODO: 渡された "token" レスポンス (openid.connect.token API 応答) を保存 + + // openid.connect.token レスポンスの id_token をデコード + DecodedJWT decoded = JWT.decode(token.getIdToken()); + Map claims = decoded.getClaims(); + logger.info("claims: {}", claims); + + var teamId = claims.get("https://slack.com/team_id").asString(); + + // 取得したアクセストークンで openid.connect.userInfo API を呼び出す実装例 + var client = Slack.getInstance().methods(); + try { + if (token.getRefreshToken() != null) { + // はじめてのトークンローテーションをするコード例 + var refreshedToken = client.openIDConnectToken(r -> r + .clientId(config.getClientId()) + .clientSecret(config.getClientSecret()) + .grantType("refresh_token") + .refreshToken(token.getRefreshToken()) + ); + + var teamIdWiredClient = Slack.getInstance().methods(refreshedToken.getAccessToken(), teamId); + var userInfo = teamIdWiredClient.openIDConnectUserInfo(r -> r.token(refreshedToken.getAccessToken())); + logger.info("userInfo: {}", userInfo); + + } else { + throw new RuntimeException("Unexpectedly refresh token is absent"); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + + // エンドユーザーにWebページを表示(または、他のサービスとの OAuth フローに続けるなどどこかにリダイレクトしてもよい) + var html = app.config().getOAuthRedirectUriPageRenderer().renderSuccessPage( + null, req.getContext().getOauthCompletionUrl()); + resp.setBody(html); + resp.setContentType("text/html; charset=utf-8"); + return resp; +}); +``` \ No newline at end of file diff --git a/docs/guides/sign-in-with-slack.md b/docs/guides/sign-in-with-slack.md new file mode 100644 index 000000000..6add3b62c --- /dev/null +++ b/docs/guides/sign-in-with-slack.md @@ -0,0 +1,146 @@ +--- +layout: default +title: "Sign in with Slack (OpenID Connect)" +lang: en +--- + +# Sign in with Slack (OpenID Connect) + +[Sign in with Slack](https://api.slack.com/authentication/sign-in-with-slack) helps users log into your service using their Slack profile. The platform feature was recently upgraded to be compatible with the standard [OpenID Connect](https://openid.net/connect/) specification. With Bolt for Java v1.10 or higher, implementing the auth flow is much easier. + +### Slack App Configuration + +When you create a new Slack app, set the following user scopes: + +```yaml +oauth_config: + redirect_urls: + - https://example.com/replace-this-with-your-own-redirect-uri + scopes: + user: + - openid # required + - email # optional + - profile # optional +``` + + +### Slack Config for Your OpenID Connect App + +Here is the list of the necessary configurations for serving your OpenID Connect app 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_CLIENT_ID**|**Client ID** (Find at **Settings** > **Basic Information** > **App Credentials**)| +|**SLACK_CLIENT_SECRET**|**Client Secret** (Find at **Settings** > **Basic Information** > **App Credentials**)| +|**SLACK_REDIRECT_URI**|**Redirect URI** (Configure at **Features** > **OAuth & Permissions** > **Redirect URLs**)| +|**SLACK_USER_SCOPES**|**Command-separated list of user scopes**: `scope` parameter that will be appended to `https://slack.com/openid/connect/authorize` as a query parameter. The possible values are `openid`, `email`, and `profile`| +|**SLACK_INSTALL_PATH**|**Starting point of OpenID Connect flow**: This endpoint redirects users to the Slack OpenID Connect endpoint with required query parameters such as `client_id`, `scope`, `state`, and `nonce` (optional).| +|**SLACK_REDIRECT_URI_PATH**|**Path for OpenID Connect Redirect URI**: This endpoint handles callback requests after the Slack's OpenID Connect confirmation. The path must be consistent with **SLACK_REDIRECT_URI** value.| + +### Examples + +Check [the Servlet app example](https://github.com/slackapi/java-slack-sdk/blob/main/bolt-servlet/src/test/java/samples/OpenIDConnectSample.java) to learn how to implement your Web app that handles the OpenID Connect flow with end-users. + +```java +import java.util.*; + +// implementation 'com.slack.api:bolt-jetty:{the latest version}' +import com.slack.api.Slack; +import com.slack.api.bolt.App; +import com.slack.api.bolt.jetty.SlackAppServer; + +// If you want to decode the JWT id_token, you can use the following external library for it: +// implementation 'com.auth0:java-jwt:{the latest version}' +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; + +// The following env variables are supposed to be set: +// SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_REDIRECT_URI, SLACK_USER_SCOPES +App app = new App().asOpenIDConnectApp(true); + +// You can handle the OpenID Connect code authorization flow with this callback function +app.openIDConnectSuccess((req, resp, token) -> { + var logger = req.getContext().getLogger(); + + // TODO: Store the given "token" response (openid.connect.token API response) + + // Decode id_token in an openid.connect.token response + DecodedJWT decoded = JWT.decode(token.getIdToken()); + Map claims = decoded.getClaims(); + logger.info("claims: {}", claims); + + var teamId = claims.get("https://slack.com/team_id").asString(); + + // Code example demonstrating how to call openid.connect.userInfo using the given access token + var client = Slack.getInstance().methods(); + try { + var userInfo = client.openIDConnectUserInfo(r -> r.token(token.getAccessToken())); + logger.info("userInfo: {}", userInfo); + + } catch (Exception e) { + throw new RuntimeException(e); + } + + // Render a web page for the user (or you can redirect the user to the next step such as OAuth with other services) + var html = app.config().getOAuthRedirectUriPageRenderer().renderSuccessPage( + null, req.getContext().getOauthCompletionUrl()); + resp.setBody(html); + resp.setContentType("text/html; charset=utf-8"); + return resp; +}); + +Map apps = new HashMap<>(); +apps.put("/slack/", app); +SlackAppServer server = new SlackAppServer(apps); +server.start(); +``` + +If you enable [the token rotation](https://api.slack.com/authentication/rotation) along with the OpenID Connect, the code can be like this: + +```java +// You can handle the OpenID Connect code authorization flow with this callback function +app.openIDConnectSuccess((req, resp, token) -> { + var logger = req.getContext().getLogger(); + + // TODO: Store the given "token" response (openid.connect.token API response) + + // Decode id_token in an openid.connect.token response + DecodedJWT decoded = JWT.decode(token.getIdToken()); + Map claims = decoded.getClaims(); + logger.info("claims: {}", claims); + + var teamId = claims.get("https://slack.com/team_id").asString(); + + // Code example demonstrating how to call openid.connect.userInfo using the given access token + var client = Slack.getInstance().methods(); + try { + if (token.getRefreshToken() != null) { + // run the first token rotation + var refreshedToken = client.openIDConnectToken(r -> r + .clientId(config.getClientId()) + .clientSecret(config.getClientSecret()) + .grantType("refresh_token") + .refreshToken(token.getRefreshToken()) + ); + + var teamIdWiredClient = Slack.getInstance().methods(refreshedToken.getAccessToken(), teamId); + var userInfo = teamIdWiredClient.openIDConnectUserInfo(r -> r.token(refreshedToken.getAccessToken())); + logger.info("userInfo: {}", userInfo); + + } else { + throw new RuntimeException("Unexpectedly refresh token is absent"); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + + // Render a web page for the user (or you can redirect the user to the next step such as OAuth with other services) + var html = app.config().getOAuthRedirectUriPageRenderer().renderSuccessPage( + null, req.getContext().getOauthCompletionUrl()); + resp.setBody(html); + resp.setContentType("text/html; charset=utf-8"); + return resp; +}); +``` \ No newline at end of file