大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【Firebase】Sign in with Slackの実装方法(設計)

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

Firebaseを用いたsign in with slackができるまでの記録第二弾として、今日は昨日1日調べてわかったことをこの記事に残しておきたいと思います。記事内容としてはあくまで設計段階ですので、実際に試してみてからまた更新します。

Firebase Authentication

Firebaseの認証部分だけを使用するために「Firebase Authentication」を使用します。この機能を使うことで認証機能をかなり簡単に実装できるようです。例えば、自分が試した以下の記事は30分程度でTwitter認証を行うことができました。

Vue、FirebaseでツイッターのOAuthを使ったログイン機能を追加する - Qiita

【該当ソースコード

<template>
  <div class="signin">
    <h2>Sign in</h2>
    <button @click="signin">Signin</button>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'Signin',
  methods: {
    signin: function () {
      const provider = new firebase.auth.TwitterAuthProvider()
      firebase.auth().signInWithPopup(provider)
        .then(
          result => {
            if (user) {
              console.log(result.user)
            } else {
              alert('有効なアカウントではありません')
            }
          })
    }
  }
}
</script>

Firebaseを通して認証を行うソースコードは以下の部分です。

const provider = new firebase.auth.TwitterAuthProvider()
firebase.auth().signInWithPopup(provider)

Twitter認証のためのメソッドとしてTwitterAuthProviderというものが用意されています。よって、slack認証のためにはSlackAuthProviderを使えばいいのだなと思ったのですが、そううまくいきませんでした。slack認証はFirebaseのプロバイダとしては登録されていないからです。

【Authenticationの画面キャプチャ】

https://i.gyazo.com/5b9a70fe16feb2c9e89d33f71cb42d39.png

ではslack認証はFirebaseに非対応なのかというと実はそうではないようです。以下の公式の記事の例でもある通り、カスタム認証システムを使用してslack認証を行うようです。

Android でカスタム認証システムを使用して Firebase 認証を行う

Slack認証フロー

カスタム認証システムを使用する上で重要なのは各APIの認証フローに沿って自作で認証フローを作ることです。slack認証フローでは公式では以下のように記載されています。


https://a.slack-edge.com/fbd3c/img/api/articles/oauth_scopes_tutorial/slack_oauth_flow_diagram.png

Token negotiation flow

  1. User arrives at your site and clicks Sign in with Slack button
  2. User arrives at slack.com/oauth/v2/authorize?client_id=CLIENT_ID&user_scope=identity.basic and briefly approves sign in
  3. User arrives at your specified redirect URL with a code parameter
  4. Your server exchanges code for an access token using slack.com/api/oauth.v2.access
  5. Your server uses the resultant access token to request user & workspace details with slack.com/api/users.identity, passing the awarded token as a HTTP authorization header or POST parameter

Sign in with Slack


ユーザーがアプリケーション(自分のアプリ)に対して認証を行い、その後slack APIAccess Tokenを発行してもらい、それを利用してリソースにアクセスするというフローです。これは至ってシンプルな認証フローだと思います。不明点がある方は以下の記事を参考にしていただければと思います。

一番分かりやすい OAuth の説明 - Qiita

上記のフローをアプリケーション上ではなくFirebase上で行う実装に置き換える必要があります。参考になる記事がありましたのでそれをそのまま使用して説明しようかと思います。

Sign in with Slack x Firebase Authenticationやってみた話 - Qiita


【Firebase版slack認証フロー】

https://camo.qiitausercontent.com/9b12ffba11311d29cae1ca40e7c1a59d163c04aa/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f32303337312f62616163303966632d383330372d363865382d356632332d6366386337623965656130312e6a706567

  1. クライアントからSlackの認証ページへ飛ぶ
  2. ユーザーはSlackの画面でアクセス許可を行う
  3. Slackは認証用codeを載せて指定しておいたリダイレクト先に飛ぶ(今回はCloud Functionsを利用)
  4. codeを使ってSlackユーザーのアクセストークンを取得、必要なら永続化などを行う
  5. FirebaseのAdmin SDKを使ってトークンを発行する
  6. トークンを載せてクライアントにリダイレクト
  7. クライアントはsignInWithCustomTokenメソッドを叩く

上の図と公式の図は同じことをしています。

https://a.slack-edge.com/fbd3c/img/api/articles/oauth_scopes_tutorial/slack_oauth_flow_diagram.png

=(イコール)

https://camo.qiitausercontent.com/9b12ffba11311d29cae1ca40e7c1a59d163c04aa/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f32303337312f62616163303966632d383330372d363865382d356632332d6366386337623965656130312e6a706567

みたいですね。

参考にしてFirebase認証をまとめてみました。

1. クライアントからSlackの認証ページへ飛ぶ

(User arrives at your site and clicks Sign in with Slack button)

これは自分のアプリケーションやサイト画面にslack認証ページに飛ぶためのボタンやリンクを設けてあげるだけです。Slack Appを作成後、Sign in with SlackのページにあるBotton Generatorを使用すれば簡単に認証ページに飛ぶことができるボタンを作成できます。

https://i.gyazo.com/19290ba07a6d2a512b47b43a04a3d5c3.png

ボタンのコードの例

<a href="https://slack.com/oauth/v2/authorize?user_scope=identity.basic&client_id=<アプリのクライアントID>"><img alt=""Sign in with Slack"" height="40" width="172" src="https://platform.slack-edge.com/img/sign_in_with_slack.png" srcset="https://platform.slack-edge.com/img/sign_in_with_slack.png 1x, https://platform.slack-edge.com/img/sign_in_with_slack@2x.png 2x" /></a>

ボタン生成ページ

Sign in with Slack

2. ユーザーはSlackの画面でアクセス許可を行う

(User arrives at slack.com/oauth/v2/authorize?client_id=CLIENT_ID&user_scope=identity.basic and briefly approves sign in)

slack上の認証画面でアクセス許可を行うフローです。上記のボタンを生成していれば問題なく認証画面に遷移すると思います。

【認証画面】

https://i.gyazo.com/239bd546d849aa0f82c7843fe2ac44b8.png

3. Slackは認証用codeを載せて指定しておいたリダイレクト先に飛ぶ(今回はCloud Functionsを利用)

(User arrives at your specified redirect URL with a code parameter)

ここはFirebaseの初学者の自分には理解するのに苦労しました。参考記事に書いてあることとしては以下になります。

  • ユーザーが許可を行うと、指定しておいたリダイレクト先に飛ぶフロー
  • ここだけ唯一サーバーサイドが必要となるのでFirebase Cloud Functionsを利用
  • その際にcodeというクエリパラメータで認証用の一時的なトークンを渡してくる

認証画面では認証後コールバック先にリダイレクトするようになっています。本来であれば自分のアプリケーションにリダイレクトするフローですが、それをFirebaseにリダイレクトするようにしています。Firebaseにリダイレクトさせるために使用するのが、Firebase Cloud Functionsというサーバーサイドの機能です。Javascript、またはTypeScriptをFirebase上にデプロイしておくことで、リダイレクトした時の処理、ロジックをFirebase上で行えるようにします。また、リダイレクト先のパラメータにcodeというクエリパラメータで認証用の一時的なトークンを渡してくれます。

デプロイ方法

Getting Started with Cloud Functions for Firebase using TypeScript - Firecasts

4. codeを使ってSlackユーザーのアクセストークンを取得、必要なら永続化などを行う

(Your server exchanges code for an access token using slack.com/api/oauth.v2.access)

先ほどのcodeを使ってoauth.v2.accessというエンドポイントへリクエストを投げ、アクセストークンを取得します。そのロジックをFirebase Cloud Functionsをデプロイして実装します。参考記事でTypeScriptのファイルが公開されていたのでこれをありがたく使わせてもらおうと思います。

uutarou10/sign-in-with-slack

※こちらの公開ファイルの最終更新日は2年前ですので更新する必要があります。例えば以下の部分。

const res = await slackClient.post<oauthAccessResponseType>('oauth.v2.access', requestArgs);
// const res = await slackClient.post<oauthAccessResponseType>('oauth.access', requestArgs);

Firebaseからslackにアクセストークンのリクエストを送るフェーズ(仮実装)

import * as functions from 'firebase-functions';
import axios from 'axios';
import * as qs from 'querystring';

// 全体的に例外処理が果てしなくてきとう。
// というか、SlackAPIはok: falseがエラーの際には返ってくるのでこれでは例外処理になってないような気がする。
// 細かいことは気にしない。

const slackClient = axios.create({
  baseURL: 'https://slack.com/api',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  transformRequest: [
    data => qs.stringify(data)
  ]
})

export type oauthAccessResponseType = {
  user_id: string;
  access_token: string;
  scope: string;
  team_name: string;
  team_id: string;
}
export const oauthAccess = async (code: string): Promise<oauthAccessResponseType> => {
  const requestArgs = {
    client_id: functions.config().slack.client_id,
    client_secret: functions.config().slack.client_secret,
    code
  };

  try {
    const res = await slackClient.post<oauthAccessResponseType>('oauth.v2.access', requestArgs);
    // const res = await slackClient.post<oauthAccessResponseType>('oauth.access', requestArgs);
    return res.data;
  } catch(e) {
    console.warn('Slack oauth was failed.', e);
    throw new Error();
  }
}

// 面倒なので使いそうなやつだけ
export type SlackUserType = {
  id: string;
  team_id: string;
  name: string;
  real_name: string;
  is_admin: boolean;
  is_owner: boolean;
  is_primary_owner: boolean;
  is_restricted: boolean;
  is_ultra_restricted: boolean;
};
export const usersInfo = async (token: string, userId: string) => {
  const requestArgs = {
    token,
    user_id: userId
  };

  try {
    const res = await slackClient.post<{user: SlackUserType}>('users.info', requestArgs);
    return res.data.user;
  } catch (e) {
    console.warn('Slack oauth was failed.', e);
    throw new Error();
  }
};

これによりアクセストークンをSlack APIから取得することができます。

5. FirebaseのAdmin SDKを使ってトークンを発行する

アクセストークン取得後Firebaseに再度リダイレクトされるため、クライアント側にカスタムトークンを発行するフローが必要になります。これもFirebase Cloud Functionsを使用して実装します。つまり、Firebase認証フローでは合計で2つのファイルをデプロイします。

トークンの発行方法としては公式の通りにcreateCustomTokenメソッドを使用します。

const customToken = await admin.auth().createCustomToken(userCredential.user_id);

カスタム トークンを作成する | Firebase

6. トークンを載せてクライアントにリダイレクト

customTokenをユーザーに返すフローです。ようやくここでユーザー側に戻ってきました。

if (redirectUri) {
      const url = new URL(redirectUri);
      url.search = `t=${customToken}`;
      res.redirect(303, url.toString());
    } else {
      res.json({
        custom_token: customToken
      }).end();
    }
    return;
  } catch (e) {
    console.error('Failed to create custom token:', e)
  }
});

7. クライアントはsignInWithCustomTokenメソッドを叩く

(Your server uses the resultant access token to request user & workspace details with slack.com/api/users.identity, passing the awarded token as a HTTP authorization header or POST parameter)

取得したカスタムトークンを使用してsignInWithCustomTokenを叩くことでAPIリソースにアクセスすることができるようになります。

firebase.auth().signInWithCustomToken(token)

【カスタム認証の該当ソースコード

github.com

終わりに

まだ上記フローを直接試していないので近日中に試してみます。