Slack認証 + Rails によるログイン画面を一から作る方法

## 概要

Rails + Slack の認証機能で簡単なログイン画面を作る。

今回はあえて slack-omniauth など専用の gem などは使わずに、一から作ってみましょう。

### お急ぎ便

すぐにぱっと試したい人は下記のコードをcloneしてきて、
環境変数をセットし、サーバを起動したら試せます。

* [slack_login](https://github.com/toririn/slack_login)

## Slack 側の用意

### Slackにログインする

* 連携したいSlackのチームにログインをしておいてください
    * [Slack](https://slack.com/)

### 連携用の鍵を作成する

* 以下にアクセスして **Create Nes App** を押下し、認証用の鍵を作成します
    * https://api.slack.com/apps

![create.jpg](https://qiita-image-store.s3.amazonaws.com/0/101975/229bd60f-1d76-5d3f-a983-f60a24aeb142.jpeg)


* 作成が出来ると作成した *Your Apps* に作成した *App* の情報が表示されるようになります

### リダイレクト先URLを設定する

* *Your Apps* から先ほど作った *App* の名前のリンクを押してください

* **OAuth & Permissions** を押下します

![oauth.jpg](https://qiita-image-store.s3.amazonaws.com/0/101975/c4a85e09-a537-eb6f-db6c-f9c9dd3de26b.jpeg)

* ユーザの認証後にリダイレクトするURLを設定します

今回は開発のローカルにリダイレクトするために下記で設定します。

```
http://localhost:3000/sessions/acs
```

※host名やポートは適宜設定してください。

設定ができたら **Save Changes** を押して設定を保存してください。

またこの値は後程出てくる `redirect_uri` の値として使うのでメモしておいてください。

### 必要なパラメータをメモしておく

* `client_id`, `secret_key` を Rails 使用するのでコピペしてメモ帳などに張り付けておいてください

![clientid.jpg](https://qiita-image-store.s3.amazonaws.com/0/101975/061a3148-2236-b05d-76d7-d1ce34c6d9db.jpeg)

※**show** ボタンを押すと `secret_key` が表示されます

以上で *Slack* 側での用意は終了です。

続いて *Rails* のセッティングをしていきます。


## Rails 側の用意

### アプリの作成

* 作業ディレクトリを作成して移動しましょう

```
mkdir slack_login
cd $_
```

* Rails アプリを作成します


```bash
bundle init
```

```ruby:Gemfile
source "https://rubygems.org"
gem "rails", '~> 5.0.0'
```

```bash
bundle
bundle exec rails new . -N --skip-active-record --skip-bundle
```

### 連携に必要なパラメータの用意

* 連携に必要なパラメータは下記になります

| パラメータ   | 説明                       |
|:-------------|:--------------------------:|
| client_id    | Slack連携を行う識別用ID    |
| secret_key   | Slack連携を行う認証用key   |
| scope        | アプリに許可する権限(とりあえず『identify』と設定しておけば良い )       |
| team_name    | 認証先のチーム名           |
| state        | リダイレクト元識別key(自分で作成)      |
| redirect_uri | 認証後のリダイレクト先     |

* `client_id`, `secret_key`, `redirect_uri` については Slack側の用意 の項目で準備したものを使います。
* `scope` はとりあえず `identify` と設定しておきます。
* `team_name` は連携をする Slack のチーム名を設定します。
* `state` は 認証時に Rails からポストする値です。これは Slack からそのまま返却されます。
    * よって `state` は自分のアプリが Slack にリクエストを送り、そのときに返ってきたレスポンスであることを証明するのに使います。

シークレットな値になるので環境変数に値をセットし、 Rails からは定数として使うようにしましょう。

* 環境変数のセット

| 環境変数           | セットする値 |
|:-------------------|:------------:|
| SLACK_CLIENT_ID    | client_id    |
| SLACK_SECRET_KEY   | secret_key   |
| SLACK_TEAM_NAME    | team_name    |
| SLACK_SCOPE        | state        |
| SLACK_REDIRECT_URL | redirect_uri |
| SLACK_STATE_CODE   | state code   |

* 環境変数を定数に入れる

お好きな gem を使うなりしてください。
今回は `easy_settings` という gem を使って定数管理をするようにします。

Gemfile に 以下の記載を追加して bundle install してください。

```ruby:Gemfile
gem 'easy_settings'
```

```
bundle
```

続いて config/settings.yml を作成します。

```yaml:config/settings.yml
default: &default
  slack:
    client_id: "<%= ENV['SLACK_CLIENT_ID'] %>" 
    secret_key: <%= ENV['SLACK_SECRET_KEY'] %>
    team_name: <%= ENV['SLACK_TEAM_NAME'] %>
    scope: <%= ENV['SLACK_SCOPE'] %>
    redirect_url: <%= ENV['SLACK_REDIRECT_URL'] %>
    state: "<%= ENV['SLACK_STATE_CODE'] %>"
    url:
      auth: https://slack.com/oauth/authorize?
      info: https://slack.com/api/oauth.access?
development:
  <<: *default

test:
  <<: *default

production:
  <<: *default                   
```

これで例えば `client_id` を取得したかったら下記のように書くことで取得できます。

```ruby
EasySettings.slack.client_id
#=> xxxxxxxxxx
```

### Slackとの連携用Classを作成

* Slackにリクエストを送信する準備

Slackにリクエストを送る準備をします。ここも自分の好きなgemなどを使ってください。
今回は `httpclient` を使います。Gemfileに以下を記載して `bundle install` してください。

```ruby:Gemfile
gem 'httpclient'
```

```
bundle
```

* 連携用Class(SlackConnector)を作成

以下のような Class を lib/ 配下に作成します。

```ruby:lib/slack_connector.rb
class SlackConnector

  def initialize
    @client_id     = EasySettings.slack.client_id.to_s
    @client_secret = EasySettings.slack.secret_key
    @scope         = EasySettings.slack.scope
    @team_name     = EasySettings.slack.team_name
    @redirect_uri  = EasySettings.slack.redirect_url
    @state         = EasySettings.slack.state
  end 

  def auth_path
    "#{auth_host}client_id=#{@client_id}&state=#{@state}&scope=#{@scope}&team=#{@team_name}&redirect_uri=#{@redirect_uri}"
  end 

  def get_info(code)
    path = "#{acs_host}client_id=#{@client_id}&client_secret=#{@client_secret}&code=#{code}&redirect_uri=#{@redirect_uri}"

    hc = HTTPClient.new
    response = hc.get(path)
    res_json = JSON.parse(response.body)

    HashWithIndifferentAccess.new(res_json)                                                                                                                                                                        
  end

  private

  def auth_host
    EasySettings.slack.url.auth
  end

  def acs_host
    EasySettings.slack.url.info
  end
end
```

#### 説明

やってることは簡単で以下の2つの処理をさせています。

1. Slack認証用のURLを作成する
2. 認証完了後の値を元にユーザの情報を取得する

* Slack認証用のURLを作成する

Slack認証用のURLにはパスパラメータで

* client_id
* state
* scope
* redirect_uri

の情報が必要です。

※各パラメータを空だとしたらこんなURLにアクセスさせる

```
https://slack.com/oauth/authorize?client_id=&state=&scope=&team=&redirect_uri=
```

これらの各パラメータを `initialize` のタイミングでインスタンス変数に格納しています。

* 認証完了後の値を元にユーザの情報を取得する

こちらもやっていることは `Slack認証用のURLを作成する` と同じです。

各パラメータをセットして、 Slack の Web API を叩き、返り値をセット返すようにしています。

ポイント二つです。

まず引数で受け取る `code` は `Slack認証用URL` からリダイレクトした後に含まれる値で、
`ログイン認証を終えたユーザを識別する値` にもなります。

次に返り値は `json` で返ってくるので `hash` へと変換をさせています。
その際文字列の `hash` に対して `symbol` でアクセスができるように
`HashWithIndifferentAccess` を使用しています。

* lib 配下を自動で読み込む設定を追加

`config/application.rb` に以下を追記

```ruby:config/application.rb
Rails.application.configure do
  config.autoload_paths += %W(#{config.root}/lib)
end
```

### Controller を作成する

ユーザに対してSlack認証させ、その結果を受け取り、セッションに格納する `Controller` を作成します。

* `Controller` と `View` を作成する

以下のコマンドで一気に作ってしまいます。

```
bundle exec rails g controller sessions index new acs
```

作成された app/controllers/sessios_controller.rb を以下のように修正します。

```ruby:controller/sessions_controller.rb
class SessionsController < ApplicationController

  def index
  end 

  def new 
    client = SlackConnector.new
    redirect_to client.auth_path
  end 

  def acs 
    state = params[:state]
    raise "invalid" if state != EasySettings.slack.state || params[:error].present?

    client = SlackConnector.new
    code   = params[:code]

    # ユーザの情報を取得
    res = client.get_info(code)

    if res[:ok]                                                                                                                                                                                                    
      raise "invalid team" if res[:team_name] != EasySettings.slack.team_name

      session[:user] = {}.tap do |u| 
        u[:id]            = res[:user_id]
        u[:access_token]  = res[:access_token]
        u[:time]          = Time.zone.now
      end 

      # ログイン成功後のリダイレクト先。今回はrootへ戻します。
      redirect_to root_path, notice: "ログインテスト成功"
    else
      redirect_to root_path, notice: "ログイン失敗"
    end 
  rescue
    redirect_to root_path, notice: "ログイン失敗"
  end 
end
```

* ログイン画面を作成

以下のように app/view/sessions/index.html.erb を修正

```erb:view/sessions/index.html.erb
<h1>Slackログイン</h1>
<%= link_to 'Slackログイン', sessions_new_path %>                                                                                                                                                                  
<% if flash[:notice] %>
  <h4><%= flash[:notice] %></h4>
<% end %>
```

* ルーティングの設定

rootにアクセスしたときにログイン画面に遷移するよう以下のパスを追加

```ruby:config/routest.rb
root controller: :sessions, action: :index
```

### 試してみる

さっそく試してみましょう。

* サーバを起動する

```
bundle
bundle exec rails s
```

* ログイン画面へ遷移する

http://localhots:3000

以下のように表示されていればOKです。

![login.jpg](https://qiita-image-store.s3.amazonaws.com/0/101975/055232c3-eabb-cbf6-efbf-03e296e00a5d.jpeg)

**Slackログインリンク** を押下しましょう。

すると Slack の認証画面へ遷移すると思います。

![slackauth.jpg](https://qiita-image-store.s3.amazonaws.com/0/101975/4726ba6b-1e51-54b2-242f-f3de3fbc8169.jpeg)


**Authrize** を押したらログイン画面に「ログイン成功」の文字が出てきます。

これでとりあえず *Slack* を利用したログイン画面は完成です。

## まとめ

今回は *Rails* と *Slack* の認証を使ったログイン画面を一から丁寧に作成していきました。

上記のコードをまとめたものを以下の *Github* にアップしています。

手元で実際に動かして試してみたい方はご利用ください。

* [slack_login](https://github.com/toririn/slack_login)

## 参考

Slack の認証後と ユーザ情報取得後のパラメータの値は以下のようになっています。

* Slack認証画面のリダイレクト後

```ruby
[1] pry(#<SessionsController>)> params
=> <ActionController::Parameters {"code"=>"xxxxx", "state"=>"xxxxx", "controller"=>"sessions", "action"=>"acs"} permitted: false>
```

* ユーザ情報取得後

```ruby
[1] pry(#<SessionsController>)> res
=> {"ok"=>true, "access_token"=>"xoxp-xxxx", "scope"=>"identify", "user_id"=>"Uxxxx", "team_name"=>"xxxx", "team_id"=>"Txxxx"}
``