【ActionCable】チャンネル接続／購読時にユーザ認証を行う

# はじめに

ActionCableを利用して、チャンネル接続／購読時にユーザ認証を行う方法と、認証に成功／失敗した時にクライアント(JavaScript)側で呼び出されるfunctionについて確認した結果について投稿させていただきます。

投稿時のバージョンは**Rails 5.2.0**です。

ActionCableの動作確認用に作成したアプリのソースコードはこちらに配置してあります。
[NaokiIshimura/action\-cable\-example](https://github.com/NaokiIshimura/action-cable-example)

# 前提条件

## 想定サービス
- 複数のチャットルームが作成できるチャットサービス
- ログインしないとサービスは利用できない
- 許可されたユーザのみチャットルームに入室できる

## クライアント(JavaScript)側の実装

```js:app/assets/javascripts/channels/chat.js
var channel = "ChatChannel"; // チャンネル名
var room    = "1";           // チャットルームID

App.cable.subscriptions.create({channel: channel, room: room}, {
    // サブスクリプションがサーバー側で利用可能になると呼び出される
    connected: function () {
      //...
    },
    // WebSocket接続が閉じると呼び出される
    disconnected: function () {
      //...
    },
    // サブスクリプションがサーバーに拒否されると呼び出される
    rejected: function () {
      //...
    },
    //...
});
```

## Cookieへのユーザ識別子(user_id)保存
Websocketサーバからはセッションにはアクセスできないが、Cookieにはアクセスできるため、ログイン成功時にCookieへuser_idを保存するようにしておく。

```ruby:app/controllers/sessions_controller.rb
# Cookieにuser_idを署名付きで保存する
cookies.encrypted[:user_id] = user.id
```

## Userモデルへのメソッド追加

ユーザがチャットルームへアクセスにアクセスできるか確認するメソッドをUserモデルに追記しておく。

```ruby:app/models/user.rb
class User < ApplicationRecord

  def can_access?(room)
    if # チャットルームへ入室するための条件を記述
      true
    else
      false
    end
  end

end
```

# ActionCableの実装

## WebSocket接続時の認証

サンプルコードが[Action Cable Overview — Ruby on Rails Guides](http://edgeguides.rubyonrails.org/action_cable_overview.html#server-side-components-connections)に紹介されていたので、そのまま利用した。

```ruby:app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user
    
    def connect
      self.current_user = find_verified_user
    end
    
    protected
    def find_verified_user
      if current_user = User.find_by(id: cookies.encrypted[:user_id])
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end
```

## チャンネル購読時の認証

サンプルコードが[http://api.rubyonrails.org/|ActionCable::Channel::Base](http://api.rubyonrails.org/classes/ActionCable/Channel/Base.html)に記載されていたので参考にした。

```ruby:app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    room = Chat.find(params['room'])
    if current_user.can_access?(room)
      stream_from "messages_#{params['room']}_channel"
    else
      reject
    end
  end
end
```

# 結果

- チャンネル接続／購読時の認証に成功するとjsで**connected**が呼び出される
- チャンネル接続時の認証に失敗するとjsで**disconnected**が呼び出される
- チャンネル接続時の認証に成功しても、購読時の認証に失敗するとjsで**rejected**が呼び出される

| チャンネル接続時の認証 | チャンネル購読時の認証 | jsで呼び出されるfunction |
|:-----------------|:-----------------|:-----------------|
| 成功              | 成功             | connected         |
| 成功              | 失敗             | rejected          |
| 失敗              | 成功             | disconnected      |
| 失敗              | 失敗             | disconnected      |


# 補足

本投稿時点では英語版と日本語版のRailsガイドでサンプルコード内のCookieの保存方法に差分がある。
英語版では「暗号化Cookie(cookies.encrypted)」日本語版では「署名付きCookie(cookies.signed)」へユーザ識別子(user_id)を保存するようにしている。

- [Action Cable Overview — Ruby on Rails Guides](http://edgeguides.rubyonrails.org/action_cable_overview.html#server-side-components-connections)
- [Action Cable の概要 \| Rails ガイド](https://railsguides.jp/action_cable_overview.html#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E5%81%B4%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88-%E6%8E%A5%E7%B6%9A)

# 参考

- [Action Cable の概要 \| Rails ガイド](https://railsguides.jp/action_cable_overview.html)
- [ActionCable::Connection::Base](http://api.rubyonrails.org/classes/ActionCable/Connection/Base.html)
- [ActionCable::Channel::Base](http://api.rubyonrails.org/classes/ActionCable/Channel/Base.html)