メインコンテンツまでスキップ

Slack でログインする (OpenID Connect)

Slack でログインする (Sign in with Slack)という機能は、ユーザーが Slack アカウントを使って他のサービスにログインすることに役立ちます。このプラットフォーム機能は、標準の OpenID Connect の仕様と互換性を持つように最近アップグレードされました。Bolt for Java の 1.10 以上のバージョンであれば、この認証フローを非常に簡単に実装することができます。

Slack アプリの設定

新しい Slack アプリをつくるときに、以下のユーザースコープを設定してください:

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_IDClient ID (Find at Settings > Basic Information > App Credentials)
SLACK_CLIENT_SECRETClient Secret (Find at Settings > Basic Information > App Credentials)
SLACK_REDIRECT_URIRedirect 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_PATHOpenID Connect フローの開始点: このエンドポイントはユーザーを client_id, scope, state, nonce (オプショナル) のクエリパラメーターとともに Slack の OpenID Connect エンドポイントにリダイレクトします。
SLACK_REDIRECT_URI_PATHOpenID Connect Redirect URI: このエンドポイントは Slack の OAuth 許可確認画面からの callback リクエストを処理します。このパスは SLACK_REDIRECT_URI の値と整合している必要があります。

コード例

どのようにエンドユーザーの OpenID Connect のフローをハンドリングするかを知るには、Servlet アプリの例を参考にしてみてください。

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<String, Claim> 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<String, App> apps = new HashMap<>();
apps.put("/slack/", app);
SlackAppServer server = new SlackAppServer(apps);
server.start();

もし、トークンローテーション(英語)の機能も同時に有効にする場合、コードは以下のようになるでしょう:

// このコールバック関数で 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<String, Claim> 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;
});