railsチュートリアルまとめ9 高度なログイン機構

# 個人的リマインド用
参考
[Ruby on Rails チュートリアル プロダクト開発の０→１を学ぼう](https://railstutorial.jp/)

## 高度なログイン機構
最近ではブラウザを再起動した後もすぐログインできる機能を備えていることが増えてきた。これは**永続cookies**を使って実装する。

### Remember me 機能
この機能を使うと、ユーザーが明示的にログアウトを実行しない限り、ログイン状態を維持することができる。

### 記憶トークンと暗号化
前回はsessionメソッドを使ってユーザーIDを保存したが、この情報はブラウザを閉じると消える。今回はセッションの永続化の第一歩として**記憶トークン(remember token)** を作成し、cookiesメソッドによる永続的cookiesの作成や、安全性の高い**記憶ダイジェスト**によるトークン認証にこの記憶トークンを活用する。

**トークン**とはパスワードの平文と同じような秘密情報。パスワードとトークンの違いは、パスワードはユーザーが作成・管理するのに対し、トークンはコンピュータが作成・管理する情報である。

**cookiesを盗み出す有名な方法は4通りある。**
1.管理の甘いネットワークを通過するネットワークパケットから**パケットスニッファ**という特殊なソフトウェアで直接cookiesを取り出す
2.データベースに保存されている記憶トークを盗み出す
3.**クロスサイトスクリプティング(XSS)** を使う
4.ユーザーがログインしているパソコンやスマホを直接操作してアクセスを奪い取る

**それぞれの対策としては、**
1.TLSをサイト全体に適用して、ネットワークデータを暗号化で保護し、パケットスニッファで読み取られないようにする
2.記憶トークンをそのままデータベースに保存するのではなく、ハッシュ値に変換して保存する
3.Railsによって自動的に対策が行われる。具体的には、ビューテンプレートで入力した内容を全て自動的にすべてエスケープする
4.システム側で対策を講じるのは困難だが、二次被害を抑えるために、ログインするたびにトークンを必ず変更したり、セキュリティ上重要になる可能性のある情報を表示する時は**デジタル署名**という暗号技術を使う

**上記の設計やセキュリティ上の考慮事項を元に、次の方針で永続的セッションを作成する**
1.記憶トークンにはランダムな文字列を生成して用いる
2.ブラウザのcookiesにはトークンを保存する時には、有効期限を設定する
3.トークンはハッシュ値に変換してからデータベースに保存する
4.ブラウザのcookiesに保存するユーザーIDは暗号化しておく
5.永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する

最後の手順が前回の、送信されたパスワードがパスワードダイジェストと一致することをauthenticateメソッドで確認することと似ている。そこで今回はremember_digest属性をUserモデルに追加する。
```
rails g migration add_remember_digest_to_users remember_digest:string
```
```
rails db:migrate
```

ここで記憶トークンとして何を使うかを決める必用がある。基本的には長くてランダムなものならなんでもいい。今回はRuby標準ライブラリのSecureRandomモジュールにあるurlsafe_base64メソッドを使う。このメソッドは、A-Z,a-z,0-9,"-","_"のいずれかの文字(64種類)からなる、長さ22のランダムな文字列を返す。
```
>> SecureRandom.urlsafe_base64
=> "brl_446-8bqHv87AQzUj_Q"
```
同一のパスワードを持っていても問題ないのと同様に、同一の記憶トークンを持っていても問題ない。一方、セッションハイジャックのリスクなどを考えると、トークンは一意であることが望ましい。urlsafe_base64で被ることはほぼない。

ユーザーを記憶するためには、記憶トークンを作成して、そのトークンをダイジェストに変換したものをデータベースに保存する。fixtureをテストする時にdigestメソッドを既に作成しているので、上の結論に従って新しいトークンを作成するためのnew_tokenメソッドを作成できる。この新しいdigestメソッドではユーザーオブジェクトが不要なので、このメソッドもUserモデルのクラスメソッドとして作成することにする。
```ruby
app/models/user.rb

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

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

  # ランダムなトークンを返す
  def User.new_token # ←ここ！
    SecureRandom.urlsafe_base64
  end
end
```
実装計画としては、user.rememberメソッドを作成することにする。このメソッドは記憶トークンをユーザーと関連づけ、トークンに対応する記憶ダイジェストをデータベースに保存する。既にremember_digestは作成済みだが、remember_token属性はまだ追加されていない。このためuser.remember_tokenメソッドを使ってトークンにアクセスできるようにし、かつ、トークンをデータベースに**保存せず**に実装する必用がある。ここでパスワードの実装と同様の手法が役に立つ。その時は仮想のpassword属性と、データベース上にあるセキュアなpassword_digest属性の2つを使った。仮想のpassword属性はhas_secure_passwordメソッドで自動的に作成されたが、今回はremember_tokenのコードを自分で書く必用がある。これを実装するため、attr_accessorを使って、「仮想の」属性を作成する。
```ruby
class User < ApplicationRecord
  attr_accessor :remember_token
  .
  .
  .
  def remember
    self.remember_token = ...
    update_attribute(:remember_digest, ...)
  end
end
```
rememberメソッドの1行目。selfというキーワードを使わないと、Rubyによってremember_tokenという名前の**ローカル変数が作成される**。selfを与えると、この代入によってユーザーのremember_token属性が期待通りに設定される。
2行目ではupdate_attributeメソッドを使って記憶ダイジェストを更新している。このメソッドはバリデーションを素通りさせる。今回はユーザーのパスワードやパスワード確認にアクセスできないので、バリデーションを素通りさせなければならない。

以上の点を考慮して、有効なトークンとそれに関連するダイジェストを作成できるようにする。具体的には、最初にUser.new_tokenで記憶トークンを作成し、続いてUser.digestを適用した結果で記憶ダイジェストを更新する。
```ruby
app/models/user.rb

class User < ApplicationRecord
  attr_accessor :remember_token # ←ここと
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

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

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember # ←ここ！
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end
```
### ログイン状態の保持
user.rememberが動作するようになったので、永続セッションを作成する準備ができた。それを行うにはcookiesメソッドを使う。このメソッドは、sessionの時と同様にハッシュとして扱える。個別のcookiesは、1つのvalueと、オプションのexpires(有効期限からできている、省略可能)。20年後に期限切れになる記憶トークンと同じ値をcookieに保存することで、永続的なセッションを作ることができる。
```
cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }
```
permanentを使うことでもっとシンプルになる
```
cookies.permanent[:remember_token] = remember_token
```
上のコードでRailsは期限を20.years.from_nowに設定する。

ユーザーIDをcookiesに保存するのは、sessionメソッドで使ったのと同じパターンを使う。
```
cookies[:user_id] = user.id
```
しかしこのままではIDが生のテキストとしてcookiesに保存されてしまう。ユーザーアカウントが奪われないためにも、**暗号化**cookiesを使う。これはcookieをブラウザに保存する前に安全に暗号化するためのもの。
```
cookies.encrypted[:user_id] = user.id
```
ユーザーIDと記憶トークンはペアで扱う必用があるので、cookieも永続化しなくてはならない。そこでencryptedとpermanentをメソッドチェーンで繋ぐ。
```
cookies.permanent.encrypted[:user_id] = user.id
```
cookiesを設定すると、以後のページのビューで下記のようにcookiesからユーザーを取り出せるようになる
```
User.find_by(id: cookies.encrypted[:user_id])
```
cookies.encrypted[:user_id]では自動的にユーザーIDのcookiesの暗号が解除され、元に戻る。
続いてbcryptを使ってcookies[:remember_token]がremember_digestと一致することを確認する。

**暗号化されたユーザーIDを使えば記憶トークンは不要ではないのか？**
記憶トークンがなければ、暗号化されたIDを奪った攻撃者は、暗号化IDをそのまま使ってお構いなしにログインしてしまう。現在の設計では、攻撃者が仮に両方のcookiesを奪い取ることに成功したとしても、本物のユーザーがログアウトすれば攻撃者はログインできなくなる。

渡されたトークンがユーザーの記憶ダイジェストと一致することを確認する。
```
BCrypt::Password.new(remember_digest) == remember_token
```
上のコードはかなり変。bcryptで暗号化されたパスワードとトークンを比較しているから。そこで、bcrypt gemの新しいソースコードでセキュアなパスワードの処理部分を詳しく調べてみると、比較に使う==演算子が**再定義**されている。つまり、下のコードと同等になる
```
BCrypt::Password.new(remember_digest).is_password?(remember_token)
```

以上のことから記憶トークンと記憶ダイジェストを比較するauthenticated?メソッドをUserモデルの中におけば良いはずだと推測できる。
```ruby
app/models/user.rb

class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }


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

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

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

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token) # ←ここ！
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end
```
authenticated?にローカル変数として定義したremember_tokenと、
attr_accessor :remember_tokenで定義したアクセサとは異なる。

これでログインしたユーザーを記憶する処理の準備が整った。
```ruby
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      reset_session
      remember user # ←ここ！
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new', status: :unprocessable_entity
    end
  end

  def destroy
    log_out
    redirect_to root_url, status: :see_other
  end
end
```
実際のSessionsヘルパーの動作は、rememberメソッド定義のuser.rememberを呼び出すまで遅延され、そこで記憶トークンを生成してトークンのダイジェストをデータベースに保存する。

続いて上と同様に、cookiesメソッドでユーザーIDと記憶トークンの永続cookiesを作成する。
```ruby
app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember(user) # ←ここ！
    user.remember
    cookies.permanent.encrypted[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # 現在ログインしているユーザーを返す（いる場合）
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end

  # 現在のユーザーをログアウトする
  def log_out
    reset_session
    @current_user = nil
  end
end
```
このコードでは、ログインするユーザーはブラウザで有効な記憶トークンを得られるように記憶されているが、前回定義したcurrent_userメソッドでは一時セッションしか扱っていないので、正常に動作しない。
```
@current_user ||= User.find_by(id: session[:user_id])
```
永続セッションの場合、session[:user_id]が存在すれば、一時セッションからユーザーを取り出し、それ以外の場合はcookies[:uesr_id]からユーザーを取り出して、対応する永続セッションにログインする必用がある。
```ruby
if session[:user_id]
  @current_user ||= User.find_by(id: session[:user_id])
elsif cookies.encrypted[:user_id]
  user = User.find_by(id: cookies.encrypted[:user_id])
  if user && user.authenticated?(cookies[:remember_token])
    log_in user
    @current_user = user
  end
end
```
user && user.authenticatedが使われていることに注目。このコードではsessionメソッドもcookiesメソッドもそれぞれ2回づつ使われていて無駄。これを解消するためには次のようなローカル変数を使う。
```ruby
if (user_id = session[:user_id]) # ←ここと
  @current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.encrypted[:user_id]) # ←ここ
  user = User.find_by(id: user_id)
  if user && user.authenticated?(cookies[:remember_token])
    log_in user
    @current_user = user
  end
end
```
```
if (user_id = session[:user_id])
```
上のコードは比較を行なっているように見えるが**代入**。意味としては、「（ユーザーIDにユーザーIDのセッションを代入した結果）ユーザーIDのセッションが存在すれば」となる。
```ruby
app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember(user)
    user.remember
    cookies.permanent.encrypted[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # 記憶トークンcookieに対応するユーザーを返す
  def current_user # ←ここ！
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.encrypted[: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

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end

  # 現在のユーザーをログアウトする
  def log_out
    reset_session
    @current_user = nil
  end
end 
```

**より複雑な問題**
上のコードでは、新しくロゴインしたユーザーは正しく記憶される。ところが、現在の設計ではユーザーが複数の端末からログインすることは想定していないため、2つ以上のブラウザでRemember me機能が使えない。

1.ブラウザAで起動し、remember meをチェックしてログインする(ハッシュ化された記憶トークンをremember_digest)に保存する
2.ブラウザBを起動しremember meをチェックしログインする(ハッシュ化された記憶トークンをremember_digestに保存し、ブラウザAが持つ記憶トークンを無効化する)
3.ブラウザAを閉じる(current_userメソッドが永続cookieを使ってログインするようになる)
4.ブラウザAを起動する(ブラウザ内に永続cookieはあるが、logged_in?がfalseを返す)

複雑になるので今回は取り上げていないが、解決策としては記憶ダイジェストを1個のテーブルとして新たに作成し、そのテーブルをユーザーのIDと紐づけることが考えられる。例えば現在のユーザーを見つける時は、そのテーブルを通して記憶ダイジェストと対応する記憶トークンをチェックするようにする。また、forgetメソッドも同様に変更し、現在使っているブラウザに対応している記憶ダイジェストのみが削除される必用がある。なお、セキュリティを考慮し、ユーザーがログアウトした場合はそのユーザーに紐づいている全てのダイジェストを削除すると良い。

### ユーザーを忘れる
ユーザーがログアウトできるようにするために、ユーザーを記憶するためのメソッドと同様の方法で、ユーザーを忘れるためのメソッドを定義する。user.forgetメソッドによってuser.rememberが取り消される。具体的には記憶ダイジェストをnilで更新する。
```ruby
app/models/user.rb

class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

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

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

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

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

  # ユーザーのログイン情報を破棄する
  def forget # ←ここ！
    update_attribute(:remember_digest, nil)
  end
end
```
forgetヘルパーメソッドを追加してlog_outヘルパーメソッドから呼び出す。下記のコードをよくみると、forgetヘルパーメソッドではuser.forgetを読んでから、user_idとremember_tokenのcookiesを削除していることがわかる。
```ruby
app/helpers/sessions_helper.rb

module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 永続的セッションを破棄する
  def forget(user) # ←ここと
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user) # ←ここ
    reset_session
    @current_user = nil
  end
end
```
### 2つの目立たないバグ
1.ユーザーは別のタブで同じサイトを開く時もある。現在は1つのタブでログアウトし、もう1つでもログアウトしようとすると失敗するようになっている。これは1度目でcurernt_userがnilになり、log_outメソッド内のforget(current_user)が失敗するから。この問題を回避するためには、**ユーザーがログイン中の時のみログアウトさせる**必用がある。
2.ユーザーが複数のブラウザでログインしていた時。例えば、ブラウザ1とブラウザ2の両方でログインしていて、かつブラウザ2にログインが記憶されている状態で、ブラウザ1からログアウトすると、この問題が発生する。
具体例：Firefoxでログアウトし、user.forgetメソッドで記憶ダイジェストがnilに設定されたとする。この場合、アプリケーションはChromeブラウザで引き続き使える。log_outメソッドによってユーザーのidが削除されるので、以下のコードの2つの条件はどちらもfalseになる。
```ruby
def current_user
  if (user_id = session[:user_id]) # ←ここと
    @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.encrypted[:user_id]) # ←ここの2つ
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end
```
その結果current_userメソッドの最終的な評価結果はnilになる。

一方、Chromeを閉じた場合は、session[:user_id]はnilになる。これはブラウザが閉じると全てのsession変数の有効期限が切れるから。しかし、user_idのcookiesはブラウザに残り続ける。つまり、Choromeを再起動すると、そのユーザーが引き続きデータベースから取得されてしまう。

結果として次のif文の条件式が評価される。
```
user && user.authenticated?(cookies[:remember_token])
```
userがnilなら一番目の条件式で評価され終了するが、実際にはnilではないので、**2番目**の条件式まで評価が進み、その時エラーが発生する。原因は、Firefoxでログアウトした時に、ユーザーのremember_digestが削除されているにもかかわらず、Chromeでアプリケーションにアクセスした時に次の文を実行するから。
```
BCrypt::Password.new(remember_digest).is_password?(remember_token)
```
上のremember_digestはnilなので、bcryptライブラリ内部で例外が発生する。解決策は、**remember_digestが存在しない時はfalseを返す処理をauthenticated?に追加する必用がある**

まずは2つのエラーをキャッチするテストから書く
```ruby
test/integration/users_login_test.rb

require "test_helper"

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_response :see_other
    assert_not is_logged_in?
    assert_redirected_to root_url
    # 2番目のウィンドウでログアウトをクリックするユーザーをシミュレートする #←ここ
    delete logout_path #←ここ
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end
```
current_userがないため2回目のdelete logout_pathの呼び出しでエラーが発生し、テストはredになる。

成功させるために、logged_in?がtrueの場合に限ってlog_outを呼び出すようにする。
```ruby
app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    log_out if logged_in? # ←ここ
    redirect_to root_url, status: :see_other
  end
end
```

2番目の問題に関してだが、2種類のブラウザのシュミレートは困難。その代わりに、同じ問題をUserモデルで直接テストするだけなら簡単に行える。記憶ダイジェストを持たないユーザーを用意し、続いてauthenticated?を呼び出す。記憶トークンが使われる前にエラーが発生するので、なんでも構わない。
```ruby
test/models/user_test.rb

require "test_helper"

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  . # ↓ここ
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end
```
上のコードではBCrypt::Password.new(nil)でエラーが発生するため、テストはredに。これをgreenにするには、記憶ダイジェストがnilの場合にfalseを返す。
```ruby
app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # 渡されたトークンがダイジェストと一致したら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
end
```
ここでは、記憶ダイジェストがnilの場合にはreturnキーワードで即座にメソッドを終了している。
### [Remember me]チェックボックス
このチェックボックスにチェックをつけてログインすれば記憶するように
```erb
<%= f.label :remember_me, class: "checkbox inline" do %>
    <%= f.check_box :remember_me %>
    <span>Remember me on this computer</span>
<% end %>
```
注意点としては、ラベルの内側に置く。
これで必用な準備は終わり。ログインフォームから送信されたparamsハッシュには既にチェックボックスの値が含まれている。
```
params[:session][:remember_me]
```
チェックボックスがオンの時'1'になり、オフの時'0'になる。
三項演算子を使い、オンの時remember(user)、オフの時にforget(user)させる
```ruby
app/controllers/sessions_controller.rb

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      reset_session
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      log_in user        #↑これ
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new', status: :unprocessable_entity
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url, status: :see_other
  end
end
```
### [Remember me]のテスト
ユーザーが記憶されるにはログインが必用。そこで、テスト内でユーザーがログインできるようにするためのヘルパーメソッドを定義することから始める。log_in_asというメソッドを作り、sessionメソッドを直接操作して、:user_idキーにuser.idの値を代入する。
```ruby
def log_in_as(user)
  session[:user_id] = user.id
end
```

統合テストでも同様のヘルパーを実装するが、統合テストではsessionを直接扱えないので、Sessionsリソースに対してpostを送信する。メソッド名は上の単体テストと同じ。

何かあった時すぐ見つけられるように、上の2つを同じ場所にまとめておく。
```ruby
test/test_helper.rb

ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  # 指定のワーカー数でテストを並列実行する
  parallelize(workers: :number_of_processors)
  # test/fixtures/*.ymlのfixtureをすべてセットアップする
  fixtures :all

  # テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end

  # テストユーザーとしてログインする
  def log_in_as(user) # ←ここと
    session[:user_id] = user.id
  end
end

class ActionDispatch::IntegrationTest

  # テストユーザーとしてログインする ←ここ
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params: { session: { email: user.email,
                                          password: password,
                                          remember_me: remember_me } }
  end
end
```
ここではテストコードの柔軟性を最大限高めるため、log_in_asメソッドではキーワード引数のパスワードと[remember me]チェックボックスのデフォルト値を、それぞれ'password'と'1'にしている。

ここでのテストの完成形
```ruby
test/integration/users_login_test.rb

require "test_helper"

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_not cookies[:remember_token].blank?
  end

  test "login without remembering" do
    # Cookieを保存してログイン
    log_in_as(@user, remember_me: '1') #←デフォルト値だから省略できるが見栄えのため
    # Cookieが削除されていることを検証してからログイン
    log_in_as(@user, remember_me: '0')
    assert cookies[:remember_token].blank?
  end
end
```
cookiesがnilかどうかのチェック。
ログイン時に「remember me」しない場合は、remember_token cookiesの値は空になるはず。しかし、実行中のアプリケーション内にある未設定のcookiesは空文字""であるにもかかわらず、テスト内部ではnilになる。nilにempty?は呼べないのでblank?を使う。
### [Remember me]をテストする
実はcurrent_user内にある複雑な分岐処理に関してはテストが行われていなかった。

テストを書き忘れている疑いがあるコードに関しては、わざとエラーは出る文を書き、そのコードブロックがテスト対象から漏れていないか確認する。
```ruby
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # 記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.encrypted[:user_id])
      raise       # テストがパスすれば、この部分がテストされていないことがわかる
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end
```
redにならなければならないのに、greenになっている。

また、log_in_asヘルパーメソッドは、どちらのバージョンもsession[:user_id]を自動的に設定するので、current_userの「remember」条件分岐を統合テストでテストするのは困難。しかし、Sessionsヘルパーのテストでcurrent_userを直接テストすれば、この制約を回避できる。
```
$ touch test/helpers/sessions_helper_test.rb
```
テスト手順
1.fixtureでuser変数を定義する
2.渡されたユーザーをrememberメソッドで記憶する
3.current_userが、渡されたユーザーと同じであることを確認する
```ruby
test/helpers/sessions_helper_test.rb

require "test_helper"

class SessionsHelperTest < ActionView::TestCase

  def setup
    @user = users(:michael)
    remember(@user)
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    assert is_logged_in?
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    assert_nil current_user
  end
end
```
さいごのnilかどうかのテストはif文内のauthenticated?の式をテストする。

またassert_equalの正しい書き方は
```
assert_equal <期待する値>, <実際の値>
assert_equal @user, current_user
```
テストはredになる。最後はraiseを取り除いておしまい。