はじめに
mixiグループ Advent Calendar 2017 の7日目の記事です。
チケットキャンプを運営している株式会社フンザでサーバサイドの開発を担当している@cgetcです。
弊社では業務でSlackを利用しており、煩雑なコミュニケーションを改善するために、SlackBotの開発を試みました。
しかし2点ほど、OAuth2.0関連で嵌まることがあったので、その事象と解決法をまとめました。
弊社はPython/Djangoで開発することが多いので、PythonでメジャーなOAuth2.0のライブラリであるpython-social-auth/social-app-djangoのサンプルを載せています。
Q. SlackのOAuth認証では認証時のScopeが指定できない
SlackのOAuth認証では認証する際にユーザ情報を取得するスコープのみが指定でき、投稿に必要なパーミッションは一度では取得できません。
なので、SlackのOAuth認証をした後にもう一度、ユーザにはスコープを変更してリンクを踏んでもらう必要があります。
(Using OAuth 2.0 Handling Multiple Authorizationsを参照ください。)
A. OAuth認証後にスコープを変更したAuthorization Request
のURLにリダイレクトさせる
Authorization Grant
のダイアログが2度表示されてしまいますが、Slackの仕様上、これがベストプラクティスだと思います。
パーミッションを許可してもらうパイプラインを実装
必要なパーミッションがあるかを確認して、ない場合にはリダイレクトさせるだけです。
リダイレクト後にも再度パイプラインを通りますが、そのときにはすでにパーミッションが付与されているので、何もせず次のパイプラインが実行されます。
from django.shortcuts import redirect
def extra_scope(backend, details, response, *args, **kwargs):
if 'chat:write:user' not in response.get('scope'):
backend.get_scope = lambda: ['chat:write:user']
auth_url = backend.auth_url()
return redirect(auth_url)
パイプラインを設定に追加
パーミッションがあるユーザと無いユーザが混在するとややこしくなるので、なるべく始めの方に定義しています。
SOCIAL_AUTH_PIPELINE = (
# Get the information we can about the user and return it in a simple
# format to create the user instance later. On some cases the details are
# already part of the auth response from the provider, but sometimes this
# could hit a provider API.
'social_core.pipeline.social_auth.social_details',
# Get the social uid from whichever service we're authing thru. The uid is
# the unique identifier of the given user in the provider.
'social_core.pipeline.social_auth.social_uid',
""" 省略 """
# Update the user record with any changed info from the auth service.
'social_core.pipeline.user.user_details',
)
Q. Add To SlackのOAuth認証でstateパラメータがない
python-socail-authのSlackバックエンドはstate
パラメータが必須になっています。しかし、Add To SlackのAuthorization Request にはstateパラメータが含まないために Authorization Responseにも含まれていません。
A. Slackバックエンドを継承したバックエンドを作成する
STATE_PARAMETER = False
にして対応します。
(stateパラメータはOAuth 2.0のCSRF対策として重要なパラメータなので、無効にするのは避けるべきです。今回はこのような対応をしましたが、state パラメータを含む方法があれば、ご教授ください。Add To SlackがボタンなどでURLを埋め込んで利用される前提なので、仕様上stateパラメータをAuthorization Requestに含めるのが難しいため、このような仕様だと思われます。)
これだけでも動きますが、Add To Slackの場合に必要な情報は、ユーザ情報ではなく、チーム情報の方が多いと思います。ですので、チーム情報を取得・格納するように get_user_details
user_data
を実装します。
from social_core.backends.slack import SlackOAuth2
class AddToSlackOAuth2(SlackOAuth2):
STATE_PARAMETER = False
def get_user_details(self, response):
"""Return user details from Slack team"""
# Build the username with the team $username@$team_url
# Necessary to get unique names for all of slack
team = response['team']
return {
'username': team['name'],
'email': '{domain}@{email_domain}'.format(**team),
'fullname': team['domain'],
}
def user_data(self, access_token, *args, **kwargs):
"""Loads user data from service"""
response = self.get_json('https://slack.com/api/team.info', params={'token': access_token})
if not response.get('id', None):
response['id'] = response['team']['id']
return response
AUTHENTICATION_BACKENDS
を上記のクラスに差し替えます。
AUTHENTICATION_BACKENDS = (
'sample.social.backends.slack.AddToSlackOAuth2',
)
認証が完了するとUser
にチーム情報が格納されます。
アクセストークンを取得するには、python-socail-authと変わりません。
from social_django.models import UserSocialAuth
def get_access_token(team_id):
return UserSocialAuth.get_social_auth('slack', team_id).extra_data['access_token']
team_id
はWebhookのPayloadなどから取得できます。
さいごに
Djangoでの例のみを示しましたが、他のWEBフレームワークでも応用できますし、SlackのOAuth2.0の実装による問題なので、言語問わず参考になるかと思います。
SlackBotの開発の助けになったら幸いです。