Edited at

python-social-authでSlackのOAuth2.0と仲良くする

More than 1 year has passed since last update.


はじめに

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の仕様上、これがベストプラクティスだと思います。


パーミッションを許可してもらうパイプラインを実装

必要なパーミッションがあるかを確認して、ない場合にはリダイレクトさせるだけです。

リダイレクト後にも再度パイプラインを通りますが、そのときにはすでにパーミッションが付与されているので、何もせず次のパイプラインが実行されます。


sample.pipline.py

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)



パイプラインを設定に追加

パーミッションがあるユーザと無いユーザが混在するとややこしくなるので、なるべく始めの方に定義しています。


settings.py


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を実装します。


sample/social/backends/slack.py

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 を上記のクラスに差し替えます。


settings.py

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の開発の助けになったら幸いです。