|
| 1 | +--- |
| 2 | +layout: ja |
| 3 | +title: "Slack でログインする (OpenID Connect)" |
| 4 | +lang: ja |
| 5 | +--- |
| 6 | + |
| 7 | +# Slack でログインする (OpenID Connect) |
| 8 | + |
| 9 | +[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 以上のバージョンであれば、この認証フローを非常に簡単に実装することができます。 |
| 10 | + |
| 11 | +### Slack アプリの設定 |
| 12 | + |
| 13 | +新しい Slack アプリをつくるときに、以下のユーザースコープを設定してください: |
| 14 | + |
| 15 | +```yaml |
| 16 | +oauth_config: |
| 17 | + redirect_urls: |
| 18 | + - https://example.com/replace-this-with-your-own-redirect-uri |
| 19 | + scopes: |
| 20 | + user: |
| 21 | + - openid # 必須 |
| 22 | + - email # オプショナル |
| 23 | + - profile # オプショナル |
| 24 | +``` |
| 25 | +
|
| 26 | +
|
| 27 | +### OpenID Connect アプリのための設定 |
| 28 | +
|
| 29 | +以下は OpenID Connect 互換のアプリのための設定項目の一覧です。もしこれら以外の環境変数名や、別の読み込みの仕組みを使いたい場合は、自前で **AppConfig** を初期化する実装を行ってください。 |
| 30 | +
|
| 31 | +|環境変数名|説明 (値を見つけられる場所)| |
| 32 | +|-|-| |
| 33 | +|**SLACK_CLIENT_ID**|**Client ID** (Find at **Settings** > **Basic Information** > **App Credentials**)| |
| 34 | +|**SLACK_CLIENT_SECRET**|**Client Secret** (Find at **Settings** > **Basic Information** > **App Credentials**)| |
| 35 | +|**SLACK_REDIRECT_URI**|**Redirect URI** (Configure at **Features** > **OAuth & Permissions** > **Redirect URLs**)| |
| 36 | +|**SLACK_USER_SCOPES**|**カンマ区切りの user scope リスト**: `scope` パラメーターは `https://slack.com/openid/connect/authorize` にクエリパラメーターとして付加されます。可能な値は `openid`, `email`, `profile` です。| |
| 37 | +|**SLACK_INSTALL_PATH**|**OpenID Connect フローの開始点**: このエンドポイントはユーザーを `client_id`, `scope`, `state`, `nonce` (オプショナル) のクエリパラメーターとともに Slack の OpenID Connect エンドポイントにリダイレクトします。| |
| 38 | +|**SLACK_REDIRECT_URI_PATH**|**OpenID Connect Redirect URI**: このエンドポイントは Slack の OAuth 許可確認画面からの callback リクエストを処理します。このパスは **SLACK_REDIRECT_URI** の値と整合している必要があります。| |
| 39 | + |
| 40 | +### コード例 |
| 41 | + |
| 42 | +どのようにエンドユーザーの OpenID Connect のフローをハンドリングするかを知るには、[Servlet アプリの例](https://github.com/slackapi/java-slack-sdk/blob/main/bolt-servlet/src/test/java/samples/OpenIDConnectSample.java)を参考にしてみてください。 |
| 43 | + |
| 44 | +```java |
| 45 | +import java.util.*; |
| 46 | +
|
| 47 | +// implementation 'com.slack.api:bolt-jetty:{the latest version}' |
| 48 | +import com.slack.api.Slack; |
| 49 | +import com.slack.api.bolt.App; |
| 50 | +import com.slack.api.bolt.jetty.SlackAppServer; |
| 51 | +
|
| 52 | +// id_token の JWT の値をでコードしたい場合は、以下の外部ライブラリを使う: |
| 53 | +// implementation 'com.auth0:java-jwt:{the latest version}' |
| 54 | +import com.auth0.jwt.JWT; |
| 55 | +import com.auth0.jwt.interfaces.Claim; |
| 56 | +import com.auth0.jwt.interfaces.DecodedJWT; |
| 57 | +
|
| 58 | +// 以下の環境変数が設定されていることが前提: |
| 59 | +// SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_REDIRECT_URI, SLACK_USER_SCOPES |
| 60 | +App app = new App().asOpenIDConnectApp(true); |
| 61 | +
|
| 62 | +// このコールバック関数で OpenID Connect の code authorization フローをハンドリングできる |
| 63 | +app.openIDConnectSuccess((req, resp, token) -> { |
| 64 | + var logger = req.getContext().getLogger(); |
| 65 | + |
| 66 | + // TODO: 渡された "token" レスポンス (openid.connect.token API 応答) を保存 |
| 67 | +
|
| 68 | + // openid.connect.token レスポンスの id_token をデコード |
| 69 | + DecodedJWT decoded = JWT.decode(token.getIdToken()); |
| 70 | + Map<String, Claim> claims = decoded.getClaims(); |
| 71 | + logger.info("claims: {}", claims); |
| 72 | +
|
| 73 | + var teamId = claims.get("https://slack.com/team_id").asString(); |
| 74 | +
|
| 75 | + // 取得したアクセストークンで openid.connect.userInfo API を呼び出す実装例 |
| 76 | + var client = Slack.getInstance().methods(); |
| 77 | + try { |
| 78 | + var userInfo = client.openIDConnectUserInfo(r -> r.token(token.getAccessToken())); |
| 79 | + logger.info("userInfo: {}", userInfo); |
| 80 | +
|
| 81 | + } catch (Exception e) { |
| 82 | + throw new RuntimeException(e); |
| 83 | + } |
| 84 | +
|
| 85 | + // エンドユーザーにWebページを表示(または、他のサービスとの OAuth フローに続けるなどどこかにリダイレクトしてもよい) |
| 86 | + var html = app.config().getOAuthRedirectUriPageRenderer().renderSuccessPage( |
| 87 | + null, req.getContext().getOauthCompletionUrl()); |
| 88 | + resp.setBody(html); |
| 89 | + resp.setContentType("text/html; charset=utf-8"); |
| 90 | + return resp; |
| 91 | +}); |
| 92 | +
|
| 93 | +Map<String, App> apps = new HashMap<>(); |
| 94 | +apps.put("/slack/", app); |
| 95 | +SlackAppServer server = new SlackAppServer(apps); |
| 96 | +server.start(); |
| 97 | +``` |
| 98 | + |
| 99 | +もし、[トークンローテーション(英語)](https://api.slack.com/authentication/rotation)の機能も同時に有効にする場合、コードは以下のようになるでしょう: |
| 100 | + |
| 101 | +```java |
| 102 | +// このコールバック関数で OpenID Connect の code authorization フローをハンドリングできる |
| 103 | +app.openIDConnectSuccess((req, resp, token) -> { |
| 104 | + var logger = req.getContext().getLogger(); |
| 105 | +
|
| 106 | + // TODO: 渡された "token" レスポンス (openid.connect.token API 応答) を保存 |
| 107 | +
|
| 108 | + // openid.connect.token レスポンスの id_token をデコード |
| 109 | + DecodedJWT decoded = JWT.decode(token.getIdToken()); |
| 110 | + Map<String, Claim> claims = decoded.getClaims(); |
| 111 | + logger.info("claims: {}", claims); |
| 112 | +
|
| 113 | + var teamId = claims.get("https://slack.com/team_id").asString(); |
| 114 | +
|
| 115 | + // 取得したアクセストークンで openid.connect.userInfo API を呼び出す実装例 |
| 116 | + var client = Slack.getInstance().methods(); |
| 117 | + try { |
| 118 | + if (token.getRefreshToken() != null) { |
| 119 | + // はじめてのトークンローテーションをするコード例 |
| 120 | + var refreshedToken = client.openIDConnectToken(r -> r |
| 121 | + .clientId(config.getClientId()) |
| 122 | + .clientSecret(config.getClientSecret()) |
| 123 | + .grantType("refresh_token") |
| 124 | + .refreshToken(token.getRefreshToken()) |
| 125 | + ); |
| 126 | +
|
| 127 | + var teamIdWiredClient = Slack.getInstance().methods(refreshedToken.getAccessToken(), teamId); |
| 128 | + var userInfo = teamIdWiredClient.openIDConnectUserInfo(r -> r.token(refreshedToken.getAccessToken())); |
| 129 | + logger.info("userInfo: {}", userInfo); |
| 130 | +
|
| 131 | + } else { |
| 132 | + throw new RuntimeException("Unexpectedly refresh token is absent"); |
| 133 | + } |
| 134 | +
|
| 135 | + } catch (Exception e) { |
| 136 | + throw new RuntimeException(e); |
| 137 | + } |
| 138 | +
|
| 139 | + // エンドユーザーにWebページを表示(または、他のサービスとの OAuth フローに続けるなどどこかにリダイレクトしてもよい) |
| 140 | + var html = app.config().getOAuthRedirectUriPageRenderer().renderSuccessPage( |
| 141 | + null, req.getContext().getOauthCompletionUrl()); |
| 142 | + resp.setBody(html); |
| 143 | + resp.setContentType("text/html; charset=utf-8"); |
| 144 | + return resp; |
| 145 | +}); |
| 146 | +``` |
0 commit comments