Cookieを利用した自動ログイン

## はじめに
Ruby on Rails　チュートリアルで学んだCookieを利用した自動ログイン
備忘録

ブラウザを閉じた後でもログインを有効にする`Remember me`を実装する
Ruby on Railsを学んでいる方・学ぶ予定の方
前提の記事を読んだ後、こちらを読んでいただければ教材を進める助けになると思います。


## 前提
https://qiita.com/d0ne1s/items/7c4d2be3f53e34a9dec7

自動ログインのない基本的なログイン機構を実装していることが前提です
上記記事でログイン機能の実装を把握した後にこちらを読んでください。
どちらもRuby on Railsチュートリアルのコードを利用しています。

*素敵な記事をお借りします。*

## `remember_digest`属性をUserモデルに追加
```rb
# 語尾の_to_usersはmigration対象がuses_tableだと明示
$ rails generate migration add_remember_digest_to_users remember_digest:string

$ rails db:migrate
```

## メソッドを追加する
`User`モデル

```rb
attr_accessor :remember_token # 仮想カラムを用意する

# 渡された文字列のハッシュ値を返す
def self.digest(string)
  cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                BCrypt::Engine.cost
  BCrypt::Password.create(string, cost: cost)
end

# ランダムなトークンを返す
def self.new_token
  SecureRandom.urlsafe_base64 # 標準のmodele
end

# 永続セッションのためにユーザーをデータベースに記憶する
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end

# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
  return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

# ユーザーのログイン情報を破棄する
def forget
  update_attribute(:remember_digest, nil)
end
```

## helperを定義する
`app/helpers/sessions_helper.rb`
```rb
# ユーザーのセッションを永続的にする
def remember(user)
  user.remember
  cookies.permanent.signed[:user_id] = user.id
  cookies.permanent[:remember_token] = user.remember_token
end
```

## ログインと連動させる
`app/controllers/sessions_controller.rb`
```rb
def create
.
.
  remember user
.
end
```

## `current_user`を更新する
*ログインと連携させた時点でcookiesには有効なidとtokenが保存されたが、保存したcookiesを使用する記述をしていない*
*有効なcookiesがあれば自動ログインできるようにする*
`app/helpers/sessions_helper.rb`
```rb
# 記憶トークンcookieに対応するユーザーを返す
def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end
```

## ユーザーを忘れる
`forget(user)`ヘルパーメソッドを作成する
`log_out`から`foget(user)`を呼び出す

`app/helpers/sessions_helper.rb`
```rb
# 永続的セッションを破棄する
  def forget(user)
    user.forget # forgetはmodelのインスタンスメソッド
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user) # これを追記
    session.delete(:user_id)
    @current_user = nil
  end
```

## 小さなバグに対応する
*2つのタブで２回ログアウトを実行した際`forget(current_user)`が失敗する*

*2種類のブラウザで利用した際、片方だけログアウトして、もう片方で再度訪れると、`remember_digest`が`nil`なのにcookiesで自動ログインしようとして、`Bcrypt`のtokenとremember_digestの比較で例外が発生する*

### ログインしている時だけログアウト処理を実行する
`app/controllers/sessions_controller.rb`
```rb
def destroy
  log_out if logged_in? # ここを編集
  redirect_to root_url
end
```

### 記憶ダイジェストが`nil`の場合に`false`を返す
`app/models/user.rb`
```rb
# 渡されたトークンがダイジェストと一致したらtrueを返す
def authenticated?(remember_token)
  return false if remember_digest.nil? # これを追記した
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
```

## チェックボックで永続的ログインを選択する

*ログインのフォームに下記を追記する*
```rb
# labelの内側に配置する配置する
<%= f.label :remember_me, class: "checkbox inline" do %>
  <%= f.check_box :remember_me %>
  <span>Remember me on this computer</span>
<% end %>
```
**チェックボックスの仕様**
*ONの時 `'1'`になり、OFの時 `'0'`になる*

*createアクションを編集する*
```rb
def create
  .
  .
  # チェックが入っている時のみtokenを記憶する 三項演算子
  params[:session][:remember_me] == '1' ? remember(user) : forget(user)
  .
end
```

## 終わりに
Ruby on RailsチュートリアルはRailsの使い方というより、Webアプリケーションの作り方を体系的に学べます。
ログイン機構の仕組みは他の言語やフレームワークを使った場合でも同じなので、書き方ではなくどういう仕組みなのかを理解したいですね。