Railsチュートリアル 第9章 発展的なログイン機構 - Remember me 機能

# 前提知識

## remember me 機能
「ユーザーのログイン情報を記憶し、ブラウザを再起動した後でも、ログイン情報を改めて入力する必要なく直ちにログイン後のユーザー情報にアクセスできるようにする」という機能です。

## 永続クッキー

英語では「permanent cookies」と呼ばれます。Webアプリケーション側が指定した期間にわたり、ユーザーの端末上に保存されるようにしたcookiesのことを指します。ブラウザプロセスの終了により破棄される「セッションクッキー(session cookies)」に対する概念です。

# 記憶トークンと暗号化

## この項で行うこと

記憶トークン(remember token)を作成します。これから実装する以下のような機能に向けての第一ステップです。

* `cookies`メソッドによる永続的cookiesの作成
* 安全性の高い記憶ダイジェスト(remember digest)によるトークン認証の実装

## セッションハイジャック

永続的cookiesと表裏一体で存在する攻撃手法として、「セッションハイジャック」という手法があります。「記憶トークンを奪って、特定のユーザーになりすましてログインする」という攻撃手法です。このような攻撃を実現するためには、以下のような手法が用いられます。

* パケットスニッファソフトにより直接cookieを取り出す
    * ネットワークもしくはアプリケーションの管理が甘く、ネットワークパケットを直接抜き取れるようなアプリケーションであることが前提
* データベースから記憶トークンを取り出す
* クロスサイトスクリプティングを使う
* ユーザーが使用している端末を直接操作してアクセスを奪い取る

### セッションハイジャックへの対策

#### 「ネットワークパケットを直接抜き取る」という攻撃手法への対策

「サイト全体で、エンドツーエンドの通信に対してSSLを適用する」という対策が代表的です。エンドツーエンドで通信が暗号化されていれば、ネットワークパケットを直接抜き取っても通信内容の機密は保たれます。`config/environments/production.rb`に対する以下の設定内容は、当該対策に関わるものですね。

```ruby:config/environments/production.rb
config.force_ssl = true
```

#### 「データベースから記憶トークンを取り出す」という攻撃手法への対策

「DBMSにおいて、生の記憶トークンではなく、記憶トークンのハッシュ値を保存するようにする」という対策が代表的です。当サンプルアプリケーションの過去の実装において、Userモデルのパスワードに対して同様の実装を行いましたよね。関連するメソッドとしては以下のようなものがあります。

```
has_secure_password
authenticate
```

#### 「クロスサイトスクリプティングを使う」という攻撃手法への対策

こちらはRailsそのものに対策の仕組みが実装されています。具体的には、「ビューのテンプレートで入力した内容を自動的にエスケープする」というものです。

#### 「ユーザーが使用している端末を直接操作してアクセスを奪い取る」という攻撃手法への対策

さすがに攻撃そのものに対してWebアプリケーション側で取れる根本的対策はありません。しかしながら、被害を最小限に留める仕組みはあります。以下のようなものです。

* ユーザーが（別端末などで）ログアウトしたときに必ずトークンを変更するようにする
* セキュリティ上重要になる可能性のある情報を表示するときはデジタル署名を行う

## 永続的セッションの実装はどのような方針をとるのか

セキュリティ上考慮するべき点を踏まえ、永続的セッションの実装は以下の方針をとることとします。

* 記憶トークンには、ランダムな文字列を生成して用いる
* ブラウザのcookiesにトークンを保存する際には、有効期限を設定する
* トークンはハッシュ値に変換してからRDBに保存する
* ブラウザのcookiesに保存するユーザーIDは暗号化しておく
* 永続ユーザーIDを含むcookiesを受け取った場合の対応
    * 受け取ったIDでRDBを検索する
    * 記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する

## `remember_digest`属性と、Userモデルの新たな設計

> トークンはハッシュ値に変換してからRDBに保存する

上記の実装方針を踏まえ、「トークンのハッシュ値を保存するための属性」がRDBに必要となります。`remember_digest`という属性名で当該属性を定義することとします。`remember_digest`属性追加後のUserモデルの定義は以下の通りになります。

![User_full.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/d9247c3a-66f0-f160-bb8f-b02825b934f7.png)

Userモデルの変更を、実際にRDBに反映します。使用するのは`rails generate migration`コマンドですね。

```
# rails generate migration add_remember_digest_to_users remember_digest:string
Running via Spring preloader in process 438
      invoke  active_record
      create    db/migrate/[timestamp]_add_remember_digest_to_users.rb
```

マイグレーションの実体である`db/migrate/[timestamp]_add_remember_digest_to_users.rb`の内容は以下の通りです。

```ruby:db/migrate/[timestamp]_add_remember_digest_to_users.rb
class AddRememberDigestToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :remember_digest, :string
  end
end
```

`password_digest`と同様、`remember_digest`もユーザーが直接読み出すことはありません（かつ、そうさせてはならない属性です）。そのため、インデックスを生成する必要もありません。生成されたマイグレーションには手を加えず、そのまま使います。

```
# rails db:migrate
== [timestamp] AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0164s
== [timestamp] AddRememberDigestToUsers: migrated (0.0169s) ================
```

### 改めて`password_digest`属性の実装を振り返る

「トークンのハッシュ値を保存するための属性」といえば、過去に実装した「パスワードのハッシュ値を保存するための属性」と似ています。先ほどのUserモデルの定義における、`password_digest`属性がそれです。このとき使ったコマンドは以下でした。

```
rails generate migration add_password_digest_to_users password_digest:string
```

マイグレーションの実体である`db/migrate/[timestamp]_add_password_digest_to_users.rb`の内容は以下の通りでしたね。

```ruby:db/migrate/[timestamp]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :password_digest, :string
  end
end
```

このとき生成されたマイグレーションに手を加えなかったのも同じです。

```
rails db:migrate
```

## 記憶トークンとして`SecureRandom.urlsafe_base64`メソッドの結果を使う

### 記憶トークンとして用いる文字列に求められる性質

記憶トークンとして用いる文字列には、以下のような性質が求められます。

* 十分な長さを有すること
* 十分にランダムであること
* （できれば）一意であること
    * 同一のトークンを持つユーザーが複数いても問題はない
    * 一方で、セッションハイジャックのリスクなどを考え、トークンは一意であるほうが安全とはいえる

### `SecureRandom`ライブラリ

Ruby標準のライブラリの一つです。[Ruby 2.6.0 リファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/class/SecureRandom.html)には、以下のような説明があります。

> 安全な乱数発生器のためのインターフェースを提供するモジュールです。 HTTP のセッションキーなどに適しています。

### `urlsafe_base64`メソッド

`SecureRandom`ライブラリに含まれる特異メソッドの一つです。`a`-`z`、`A`-`Z`、`0`-`9`、`-`、`_`のみから成るランダムな文字列を返します。引数を指定しなければ、返ってくる文字列の文字数は22となります。

```
# rails console
>> SecureRandom.urlsafe_base64
=> "eBnhzEgyNc1zWD83O1SQZQ"
```

上記は`SecureRandom.urlsafe_base64`メソッドの典型的な実行例です。

#### `SecureRandom.urlsafe_base64`の実行結果が意味するところ

引数なしの`urlsafe_base64`の実行結果は、「64種類の文字からなる長さ22のランダムな文字列」です。ランダム性が十分であれば、このメソッドの実行結果がたまたま完全に一致する可能性は、「$1/64^{22}=2^{-132}\approx10^{-40}$」となります。衝突の可能性は無視できるほど低い確率であり、一意性は十分といえます。

また、メソッド名に urlsafe という語が入っているように、`urlsafe_base64`の実行結果は、エスケープなしにURLに埋め込むことができます。URLを扱う操作においても、特段の配慮なしに使うことができるわけなのです。URLでトークンを扱う操作の例を以下に列挙します。

* アカウント有効化のリンクを発行する
* パスワードリセットのリンクを発行する

## Userモデルにトークン生成用メソッドを追加する

```diff:app/models/user.rb
  class User < ApplicationRecord
    ...略
+
+   # ランダムなトークンを返す
+   def User.new_token
+     SecureRandom.urlsafe_base64
+   end
  end
```

ここで重要なのは、「`new_token`メソッドは、Userモデルのオブジェクトを必要としない。ゆえに（インスタンスメソッドではなく）クラスメソッドとして定義する」という実装の方針です。

同じくUserモデル内のクラスメソッドとして、「渡された文字列のハッシュ値を返す」という`digest`メソッドが既に実装されています。そのコードは以下です。

```ruby:app/models/user.rb（抜粋）
# 渡された文字列のハッシュ値を返す
def User.digest(unencrypted_password)
  cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
  BCrypt::Password.create(unencrypted_password, cost: cost)
end
```

`new_token`メソッドと`digest`メソッドにより、「ランダムな文字列を生成し、ハッシュ値に変換してからRDBに保存する」という一連の動作が実現できます。

## `user.remember`メソッドの作成

### `user.remember`メソッドとは

`user.remember`メソッドは、以下の動作をするメソッドです。

* 記憶トークンをユーザーと関連付ける
* トークンに対応する記憶ダイジェストをRDBに保存する

### `remember_token`属性

生の記憶トークンそのもの（`remember_token`属性とします）は、RDBには保存せず、ユーザー環境のcookiesに保存されます。「Userモデルには`remember_token`属性を定義しつつ、RDBには`remember_token`属性を定義しない」という実装が求められます。

この関係は、Userモデル上のパスワードの実装における「RDBに定義される`password_digest`属性と、RDBには定義されない`password`および`password_confirmation`属性との関係」に類似しています。しかしながら、パスワードの実装における`has_secure_password`メソッドのような仕組みは、残念ながらトークンの実装では使えません。`remember_token`属性のコードは自分で書く必要があります。

### `attr_accessor`メソッドと、`remember_token`属性の定義

`remember_token`は、Userモデルのインスタンス変数として定義します。cookiesにより永続化され、Userモデルの外から読み書きが行われるインスタンス変数であるため、`attr_accessor`メソッドによりUserモデルの外から読み書きができるようにします。コードは以下です。

```ruby:
attr_accessor :remember_token
```

### `user.remember`メソッドの実装

`user.remember`メソッドの実装は以下になります。

```ruby:
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end
```

以下が重要なポイントです。

* `self.remember_token`により、`@remember_token`に値を代入している
    * より厳密には、`@remember_token`に値をセットする`remember_token`メソッドが呼び出されている
* RDBへの保存処理が`update_attribute`により行われている
    * `save`でもなければ`update_attributes`でもない
    * バリデーションを素通りさせる必要があるため

## 最終的に`app/models/user.rb`に追加する内容

```diff:app/models/user.rb
  class User < ApplicationRecord
+   attr_accessor :remember_token
    ...略
+
+   # 永続セッションのためにユーザーをデータベースに記憶する
+   def remember
+     self.remember_token = User.new_token
+     update_attribute(:remember_digest, User.digest(remember_token))
+   end
  end
```

この時点でテストは正しく完了します。

```
# rails test
Running via Spring preloader in process 478
Started with run options --seed 51133

  24/24: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.72491s
24 tests, 68 assertions, 0 failures, 0 errors, 0 skips
```

>  [リスト 9.3](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-user_model_remember)では、明示的にUserクラスを呼び出すことで、新しいトークンやダイジェスト用のクラスメソッドを定義しました。実際、`User.new_token`や`User.digest`を使って呼び出せるようになったので、おそらく最も明確なクラスメソッドの定義方法であると言えるでしょう。しかし実は、より「Ruby的に正しい」クラスメソッドの定義方法が２通りあります。1つはややわかりにくく、もう1つは非常に混乱するでしょう。

こうした技巧的な派生コードについては、演習2.1.および演習2.2.で触れてみることにします。

> ヒント: selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、[リスト 9.4](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-token_digest_self)や[リスト 9.5](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-token_digest_class_self)の文脈では、selfはUser「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。 

# 演習 - 記憶トークンと暗号化

## 1.1. コンソールを開き、データベースにある最初のユーザーを変数`user`に代入してください。その後、その`user`オブジェクトから`remember`メソッドがうまく動くかどうか確認してみましょう。

```
# rails console --sandbox

>> user = User.find(1)
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2019-10-22 22:46:59", updated_at: "2019-10-22 22:46:59", password_digest: "$2a$10$j21OfGX82PY0/BqDcapJmeeo/xaVKgSQ9pEZD8hAp4B...", remember_digest: nil>

>> user.remember
   (0.1ms)  SAVEPOINT active_record_1
  SQL (9.0ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2019-11-05 10:08:44.739223"], ["remember_digest", "$2a$10$6l.Ri3zvU7ux2jQ5nvftC.irkIhGI/BdhYO.K7hDwyDtSY/z7.z8e"], ["id", 1]]
   (0.2ms)  RELEASE SAVEPOINT active_record_1
=> true

>> user 
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2019-10-22 22:46:59", updated_at: "2019-11-05 10:08:44", password_digest: "$2a$10$j21OfGX82PY0/BqDcapJmeeo/xaVKgSQ9pEZD8hAp4B...", remember_digest: "$2a$10$6l.Ri3zvU7ux2jQ5nvftC.irkIhGI/BdhYO.K7hDwyD...">
```

SQLの`UPDATE`文が正常に発行され、`remember`メソッドそのものも`true`を返してきています。`remember`メソッドはうまく動いていると判断できます。

`UPDATE`文中に`remember_token`が存在しないのは一つのポイントです。

## 1.2. `remember_token`と`remember_digest`の違いも確認してみてください。

```
>> user.remember_token
=> "Iiq68oTYkHXriAF-aFx83w"

>> user.remember_digest
=> "$2a$10$6l.Ri3zvU7ux2jQ5nvftC.irkIhGI/BdhYO.K7hDwyDtSY/z7.z8e"
```

「何らかの関数により生成されたランダムな文字列」という点は同じですが、文字数や使用する文字に違いが見られます。

文字数は以下のとおりです。

* `remember_token`…22文字
* `remember_digest`…60文字

`remember_token`はURL安全な文字のみで構成される一方、`remember_digest`にはURL安全でない文字（`/`など）が含まれます。

`remember_digest`は、`$2a$10$`という文字列で始まっています。この部分の内容はBCryptの仕様で決められています。

* `$2a$`…BCryptのバージョンを表す
* `10$`…コストパラメータが10であることを表す

```
>> user.inspect
=> "#<User id: 1, name: \"Rails Tutorial\", email: \"example@railstutorial.org\", created_at: \"2019-10-22 22:46:59\", updated_at: \"2019-11-05 10:08:44\", password_digest: \"$2a$10$j21OfGX82PY0/BqDcapJmeeo/xaVKgSQ9pEZD8hAp4B...\", remember_digest: \"$2a$10$6l.Ri3zvU7ux2jQ5nvftC.irkIhGI/BdhYO.K7hDwyD...\">"
```

「`remember_token`属性は、`inspect`メソッドの結果に表示されない」というのも大きな特徴です。`inspect`の結果に表示させないようにするために、何らかの仕組みがあるのでしょうか。

## 2.1. テストスイートを実行して、ややわかりにくい[リスト 9.4](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-token_digest_self)の実装で正しく動くことを確認してみてください。

```diff:app/models/user.rb
  class User < ApplicationRecord
    attr_accessor :remember_token
    ...略
    # 渡された文字列のハッシュ値を返す
-   def User.digest(unencrypted_password)
+   def self.digest(unencrypted_password)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
      BCrypt::Password.create(unencrypted_password, cost: cost)
    end
+
+   # ランダムなトークンを返す
+   def self.new_token
+     SecureRandom.urlsafe_base64
+   end
+
+   # 永続セッションのためにユーザーをデータベースに記憶する
+   def remember
+     self.remember_token = User.new_token
+     update_attribute(:remember_digest, User.digest(remember_token))
+   end
  end
```

```
# rails test
Running via Spring preloader in process 606
Started with run options --seed 56238

  24/24: [=================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.14859s
24 tests, 68 assertions, 0 failures, 0 errors, 0 skips
```

テストは通りました。

```
# rails console --sandbox
>> user=User.find(1)

>> user.remember    
   (0.1ms)  SAVEPOINT active_record_1
  SQL (11.8ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2019-11-06 10:03:03.340289"], ["remember_digest", "$2a$10$eu2GAXNC3acJtvcE4i2/NeVOwr06wWCDIw9Sefyd7wigubxpx75zS"], ["id", 1]]
   (0.2ms)  RELEASE SAVEPOINT active_record_1
=> true
```

演習 1.1. の処理も正しく実行できることが確認できました。

## 2.2. テストスイートを実行して、非常に混乱しやすい[リスト 9.5](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-token_digest_class_self)の実装でも正しく動くことを確認してみてください。

```diff:app/models/user.rb
  class User < ApplicationRecord
    attr_accessor :remember_token
    ...略

-   # 渡された文字列のハッシュ値を返す
-   def self.digest(unencrypted_password)
-     cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
-     BCrypt::Password.create(unencrypted_password, cost: cost)
-   end
-
-   # ランダムなトークンを返す
-   def User.new_token
-     SecureRandom.urlsafe_base64
-   end
+   class << self
+     # 渡された文字列のハッシュ値を返す
+     def digest(unencrypted_password)
+       cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+       BCrypt::Password.create(unencrypted_password, cost: cost)
+     end
+
+     # ランダムなトークンを返す
+     def new_token
+       SecureRandom.urlsafe_base64
+     end
+   end

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

```
# rails test
Running via Spring preloader in process 632
Started with run options --seed 12033

  24/24: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.46399s
24 tests, 68 assertions, 0 failures, 0 errors, 0 skips
```

テストは通りました。

```
# rails console --sandbox
>> user=User.find(1)

>> user.remember    
   (0.1ms)  SAVEPOINT active_record_1
  SQL (12.8ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2019-11-06 10:15:34.199120"], ["remember_digest", "$2a$10$l1/8SG9CKjVxTqc1F4Ffp.xsHucOWMNLcTlEpkKxqYwEsmxlDl2Yq"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
```

演習 1.1. の処理も正しく実行できることが確認できました。

# ログイン状態の保持

## これから何をするか

ここまでの実装で、`user.remember`メソッドが動作するようになりました。これにより、「ユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する」という処理が実装できる準備が整ったことになります。

## `cookies`メソッド

上述の処理を実際に行うには、`cookies`メソッドを使います。`cookies`も、`session`と同様、ハッシュのような形で使うことができます。

個別のcookiesは、1つの`value`（値）と、オプションの`exprires`（有効期限）からなります。例えば以下のコードは、「記憶トークンと同じ値を有効期限20年のcookiesに保存する」というものです。

```ruby:
cookies[:remember_token] = { value:    remember_token,
                             exprires: 20.years.from_now.utc }
```

Rails界隈においては、一般に、有効期限20年のcookiesをもって「永続cookies」とされます。`cookies.permanent`メソッドを用いた以下のコードも、上述`exprires: 20.years.from_now.utc`を用いたコードと同じ動作になります。

```ruby:
cookies.permanent[:remember_token] = remember_token 
```

### ユーザーIDをcookiesに保存する基本的な方法

`sessions`と記法としては同じです。単に`sessions`が`cookies`に変わっただけといえます。

```ruby:
cookies[:user_id] = user_id
```

ただし、`cookies`メソッドをそのまま使うと、保存内容は生のテキスト（平文）になってしまいます。「アプリケーションのcookiesの形式が外部から見えてしまう」というのは、セキュリティ上あまりよろしいとはいえません。

### 署名付きcookieにより、ユーザーIDをcookiesに安全に保存する

そこで、cookieの安全性を保つ技術である「署名付きcookie」を使うようにします。署名付きcookieを使う際に用いるメソッドは`cookies.signed`です。

```ruby:
cookies.signed[:user_id] = user_id
```

`cookies.signed`は、内部で以下の処理を行っています。

* cookiesの暗号化
* cookiesへのデジタル署名の付与

### `cookies.permanent.signed`の意味

`cookies.permanent`と`cookies.signed`はメソッドチェーンで繋いで使用することができます。

ユーザーIDと記憶トークンはペアで用いられます。そのため、cookieも永続化される必要があります。`cookies.permanent.signed`というのは、「署名付きcookieを永続化する」という意味にほかなりません。

```ruby:
cookies.permanent.signed[:user_id] = user_id
```

### 設定されたcookiesを使う

設定されたcookiesからは、`cookies.signed`メソッドにより情報を取り出すことができます。今回用いるcookiesには、暗号化された状態で情報が保存されていますが、`cookies.signed`には、暗号化を解除する機能も実装されています。

```ruby:
User.find_by(id: cookies.signed[:user_id])
```

上記コードは、「cookiesに保存されたユーザーIDをキーとして、Userモデルに紐付けされたRDBを検索する」という動作をします。

## 記憶トークンの一致を確認する

```ruby:
BCrypt::Password.new(password_digest) == remember_token
```

### この場面における`==`演算子（メソッド）および`is_password?`メソッドの意味

この場面で`==`が使えるようになるに至るプロセスは、実にRubyらしいプロセスです。bcrypt gemのソースコードである`bcrypt-ruby/lib/bcrypt/password.rb`を見ると、演算子とされる`==`そのものが再定義されているのです。

```ruby:
def ==(secret)
  super(BCrypt::Engine.hash_secret(secret, @salt))
end
alias_method :is_password?, :==
```

「`==`メソッドの引数をハッシュ化した上で、`==`メソッドのレシーバと、通常の`==`によって比較する」という動作をする、と説明できます。また、同じメソッドが、論理値を返す`is_password?`メソッドという名称でも定義されています。

結果、以下3つのコードは同じ意味になります。

```ruby:
BCrypt::Password.new(password_digest) == remember_token
BCrypt::Password.new(password_digest).==(remember_token)
BCrypt::Password.new(password_digest).is_password?(remember_token)
```

「論理値を返すこと」を明確にするために、今回は`is_password?`メソッドを使います。

```ruby:
BCrypt::Password.new(password_digest).is_password?(remember_token)
```

## Userモデルに、記憶トークンを確認する`authenticated?`メソッドを定義する

`app/models/user.rb`に、以下のように`authenticated?`メソッドの定義を追加していきます。

```diff:app/models/user.rb
  class User < ApplicationRecord
    attr_accessor :remember_token
    ...略
+
+   # 渡されたトークンがダイジェストと一致したらtrueを返す
+   def authenticated?(remember_token)
+     BCrypt::Password.new(remember_digest).is_password?(remember_token)
+   end
  end
```

`authenticated?`メソッドの実装においては、以下が重要なポイントです。

* `authenticated?`メソッドの引数の`remember_token`について
    * インスタンス変数である`self.remember_token`と同じ名前を使っている
    * インスタンス変数である`self.remember_token`とは異なる実体を指す
* `authenticated?`メソッド内の`remember_digest`について
    * インスタンス変数である`self.remember_digest`と同じ実体を指す
    * `remember_token`とは異なり、`self.`が省略できる

この挙動の違いを説明するにあたり、ポイントになるのは、「`remember_digest`はRDBのカラムと紐付けされている。一方`remember_token`はRDBのカラムと紐付けされていない。」という点です。

`attr_accessor`などによってゲッター・セッターメソッドが定義されたインスタンス変数の場合、ゲッター・セッターメソッドを呼び出す際に`self.`は省略できません。しかしながら、「Active RecordによってRDBのカラムと紐付けされたインスタンス変数」については、Active Recordに実装された仕組みにより、`self.`を省略しても参照できる形でインスタンス変数が定義されます。そういえば、`name`や`email`も`self.`なしでUserモデルのインスタンス変数に直接アクセスしていましたね。

## Sessionsヘルパーに、永続Cookiesへの保存処理を実装する

```diff:app/helpers/sessions_helper.rb
  module SessionsHelper

    ...略
+
+   # ユーザーのセッションを永続的にする
+   def remember(user)
+     user.remember
+     cookies.permanent.signed[:user_id] = user.id
+     cookies.permanent[:remember_token] = user.remember_token
+   end

    ...略
  end
```

今回追加した`SessionsHelper#remember`では、以下の処理を行っています。

* ユーザーの`remember_digest`属性を更新する
* 暗号化したユーザーのIDを永続cookiesに保存する
* ユーザーの記憶トークンを永続cookiesに保存する

## Sessionsコントローラーに、永続cookiesへの保存処理を実装する

`SessionsHelper#remember`までの実装が済んでいるなら、`app/controllers/sessions_controller.rb`には`remember user`の1行を追加するだけです。

```diff:app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    ...略
    def create
      user = User.find_by(email: params[:session][:email].downcase)
      if user && user.authenticate(params[:session][:password])
        log_in user
+       remember user
        redirect_to user
      else
        ...略
      end
    end

   ...略
  end
```

## 永続cookiesによるログインを実装する

```ruby:app/helpers/sessions_helper.rb
def current_user
  if session[:user_id]
    @current_user ||= User.find_by(id: session[:user_id])
  end
end
```

「ログイン中のユーザー」を表す`current_user`メソッドの現在の実装は、上記のとおりです。しかしながら現在の実装は、一時セッションしか扱っていません。永続セッションを扱えるようにするためには、`current_user`メソッドの実装を変更する必要があります。

新たな`current_user`メソッドの実装方針は以下です。

1. `session[:user_id]`が存在すれば、`session[:user_id]`でログイン
2. `session[:user_id]`が存在しなければ、`cookies[:user_id]`からユーザーを取り出し、対応する永続セッションにログイン

### 基本的な実装内容

```ruby:current_userメソッドの新たな実装（基本）
def current_user
  if session[:user_id]
    @current_user ||= User.find_by(id: session[:user_id])
  elsif  cookies.signed[:user_id]
    user = User.find_by(id: cookies.signed[:user_id])
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end
```

`user && user.authenticated?`というのは、「ユーザーが存在し、かつ認証トークンが正しければ`true`」という意味になります。基本的なログイン機構の実装でも同様のコードが登場しました。

ただ、この実装内容では、`session`メソッドも`cookies.signed`メソッドもそれぞれ2回呼び出されることになります。まだ改善の余地がありそうですね。

### 基本的な実装内容からのリファクタリング

ということで、ローカル変数を使ってコードを改善します。

```ruby:current_userメソッドの新たな実装（改善語）
def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(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
```

`session`・`cookies.signed`、いずれのメソッドも呼び出しは1回限りになりました。ただ、そのコードの中身については、少々紛らわしいので説明が必要です。

### `if (user_id = foobar)`の意味

```ruby:
if (user_id = session[:user_id])
```

例えば上記のコードは、下記のコードと同様の意味になることを想定して使われています。「(ユーザーIDにユーザーIDのセッションを代入した結果) ユーザーIDのセッションが存在すれば」という意味です。

```ruby:
unless (user_id = session[:user_id]).nil?
```

ポイントは以下です。

* `==`ではなく`=`である
    * 等価比較ではなく代入である
* Rubyにおいては、オブジェクトを真偽値に変換する(`!!`)と、`false`と`nil`以外は全て`true`として評価される
    * この場面で`user_id`が`false`になることは想定されない

## 実際の`current_user`メソッドの定義

以上を踏まえた上で、`app/helpers/sessions_helper.rb`に`current_user`メソッドを定義します。

```diff:app/helpers/sessions_helper.rb
  module SessionsHelper
    ...略

+   # 現在ログイン中のユーザーを返す（いる場合）
+   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

    ...略
  end
```

無事永続cookiesが発行されるようになります。以下は、実際に発行された永続cookiesの例です。

<img width="301" alt="スクリーンショット 2019-11-08 19.31.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/a0fa9547-6215-8b0b-56d3-711b08fb9b92.png">

<img width="301" alt="スクリーンショット 2019-11-08 19.28.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/4fa2e471-06bf-fadb-d8a2-5c3d8f0d1b9b.png">

名前もきちんと`user_id`と`remember_token`ですね。

## ログアウトできない！

現状では、（cookiesを直接削除するか、20年経過しない限り）このアプリケーションからログアウトする手段がありません。テストも通りません。

```
# rails test
Running via Spring preloader in process 107
Started with run options --seed 55952

 FAIL["test_login_with_valid_information_followed_by_logout", UsersLoginTest, 2.62310190000062]
 test_login_with_valid_information_followed_by_logout#UsersLoginTest (2.62s)
        Expected at least 1 element matching "a[href="/login"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_login_test.rb:33:in `block in <class:UsersLoginTest>'

  24/24: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.62620s
24 tests, 66 assertions, 1 failures, 0 errors, 0 skips
```

`test/integration/users_login_test.rb`の33行目において「/login へのハイパーリンクが1つ見つからなければならないのに、実際には1つも見つからない」ということでテストが失敗しています。

```ruby:test/integration/users_login_test.rb（29〜33行目）
delete logout_path
assert_not is_logged_in?
assert_redirected_to root_url
follow_redirect!
assert_select "a[href=?]", login_path
```

* /logout に対して`delete`アクションが正常に発行され、正常に完了しいる
    * `session[:user_id]`が`nil`になる
    * リダイレクト先は / である
* しかしながら、リダイレクト先の / に「Log in」リンクは存在しない

次に実装すべきは、永続cookiesが実装された状態からユーザーがログアウトする機能です。

# 演習 - ログイン状態の保持

## 1. ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう。

上述「実際の`current_user`メソッドの定義」の通りです。

## 2. コンソールを開き、[リスト 9.6](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-authenticated_p)の`authenticated?`メソッドがうまく動くかどうか確かめてみましょう。

以下のユーザーが対象であることを前提とします。

<img width="516" alt="スクリーンショット 2019-11-08 21.52.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/888b2658-675c-d256-a866-2f5e4f22569b.png">

また、以下の永続cookiesが存在することを前提とします。

<img width="801" alt="スクリーンショット 2019-11-08 21.49.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/33c3c25d-f0e0-01f3-f57f-dd74788a102d.png">

`remember_token`の値は`iwYAfqnByUCCT7RnWS4IgQ`となっています。

```
# rails console --sandbox

>> user = User.find(2)
=> #<User id: 2, ...略>
>> user.authenticated?("iwYAfqnByUCCT7RnWS4IgQ")
=> true

>> SecureRandom.urlsafe_base64
=> "Eky2ZYTXB_a7gq-s5fv4ig"
>> user.authenticated?("Eky2ZYTXB_a7gq-s5fv4ig")
=> false
```

* 永続cookiesの`remember_token`の値を引数とすると`true`が返ってきた
* `SecureRandom.urlsafe_base64`で適当に生成したトークンを引数とすると`false`が返ってきた

上述の結果から、`authenticated?`メソッドはうまく動いているようです。

# ユーザーを忘れる

## Userモデルに`forget`メソッドを実装する

永続cookiesが実装された状態からユーザーがログアウトする機能として、Userモデルに`forget`メソッドを実装します。具体的には、「`remember_digest`属性の内容を`nil`で上書きする（そして、その変更をRDBに保存する）」という動作をするメソッドです。

```ruby:
def forget
  update_attribute(:remember_digest, nil)
end
```

Userモデルなので、コードを追加するのは`app/models/user.rb`です。

```diff:app/models/user.rb
  class User < ApplicationRecord
    attr_accessor :remember_token
    ...略
+
+   # ユーザーのログイン情報を破棄する
+   def forget
+     update_attribute(:remember_digest, nil)
+   end
+ end
```

## Sessionsヘルパーに追加する、永続的セッションを破棄する`forget`メソッド

内容は以下のとおりです。

```ruby:
def forget(user)
  user.forget
  cookies.delete(:user_id)
  cookies.delete(:remember_token)
end
```

Sessionsヘルパーの`forget`メソッドで実際に行う処理は以下のとおりとなります。

* Userモデルの`forget`メソッドを呼び出している
    * RDBに保存された`remember_digest`を破棄する
* 以下の永続cookiesを削除している
    * 暗号化されたユーザーID
    * 認証トークン

## Sessionsヘルパーに、永続的セッションを破棄してログアウトする処理を追加する

`app/helpers/sessions_helper.rb`に、`forget`メソッド、ならびに`log_out`メソッド内での`forget`メソッドの呼び出しを追加します。

```diff:app/helpers/sessions_helper.rb
  module SessionsHelper
    ...略
+
+   # 永続的セッションを破棄する
+   def forget(user)
+     user.forget
+     cookies.delete(:user_id)
+     cookies.delete(:remember_token)
+   end

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

## 改めてテストを実行する

```
# rails test
Running via Spring preloader in process 136
Started with run options --seed 25135

  24/24: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.09096s
24 tests, 68 assertions, 0 failures, 0 errors, 0 skips
```

この時点でテストは問題なく通ります。

# 演習 - ユーザーを忘れる

## 1. ログアウトした後に、ブラウザの対応するcookiesが削除されていることを確認してみましょう。 

<img width="1437" alt="スクリーンショット 2019-11-08 22.45.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/1b25ad2d-604d-c893-133e-535d9d8b5789.png">

ログイン中の時点では、`remember_token`と`user_id`という、有効期限が20年後に設定された2つのcookiesが存在します。

<img width="1437" alt="スクリーンショット 2019-11-08 22.45.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/032627da-36ac-f2bb-cd77-e9e960dd8da3.png">

ログアウトすると、`remember_token`と`user_id`という2つのcookiesはいずれも削除されました。

# ２つの目立たないバグ

## バグの内容

### その1「複数のタブから複数回ログアウト操作をするとエラーになる」

以下の操作をすると、現状ではRailsのエラー画面が表示される事態が発生してしまいます。

1. 同一ブラウザの複数タブ（または複数ウィンドウ）で、ログイン中のユーザーページを開く
2. 1つのタブで「Log out」をクリックし、ログアウト操作を行う
3. 別のタブで「Log out」をクリックする

エラー画面のスクリーンショットを以下に掲載します。発生するエラーは、`SessionsController#destroy`の`NoMethodError`エラーです。

<img width="1055" alt="スクリーンショット 2019-11-09 23.46.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/a0e0cbfa-b487-3d0e-ce7c-26768dfa4db1.png">

エラーの原因は、「`current_user`が`nil`であるにもかかわらず、`current_user`を引数とした`forget`メソッドを呼び出してしまう」という操作にあります。しかしながら、「ユーザーが通常のブラウジングで行いうる操作に対して例外を投げてしまう」というのはよろしくありません。

### その2「複数のWebブラウザでログインし、そのうちの一つからログアウトすると、別のWebブラウザを終了して再起動したときにエラーになる」

以下の操作をすると、現状ではRailsのエラー画面が表示される事態が発生してしまいます。

1. 複数のWebブラウザ（例えばFirefoxとSafari）で、特定のユーザーにログインする
2. 1つのWebブラウザ（例えばFirefox）で「Log out」をクリックし、ログアウト操作を行う
3. 別のWebブラウザ（例えばSafari）で、ログアウトせずに終了する
4. ログアウトしていないほうのWebブラウザ（例えばSafari）を再起動し、アプリケーションの任意のページを表示する

エラー画面のスクリーンショットを以下に掲載します。発生するエラーは、表示しようとしたページの`BCrypt::Errors::InvalidHash`エラーです。今回は表示しようとしたページが / なので、エラーは`StaticPages#home`で発生しています。

<img width="1245" alt="スクリーンショット 2019-11-09 23.57.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/968f8e6a-87c8-a078-4dc7-d76d16c8b862.png">

エラーの原因は、「一つのWebブラウザでログアウト操作を行った時点でRDBから`remember_digest`の属性値が削除されるにもかかわらず、別のWebブラウザから`remember_token`が入力されてしまう」という操作にあります。

#### もう少し詳しい説明

1つ目のWebブラウザでログアウトすると、Sessionsヘルパーの`log_out`メソッドが呼び出されます。結果、以下の処理が行われます。

* 1つ目のWebブラウザの永続cookiesが削除される
* 1つ目のWebブラウザの一時cookiesが削除される

結果、以下の`current_user`メソッドの戻り値は`nil`になります。

```ruby:
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
```

一方、2つ目のWebブラウザの内部状態は以下のようになります。

* 2つ目のWebブラウザのプログラムを終了した時点で、2つ目のWebブラウザの一時cookiesは破棄される
* 一連の動作において、2つ目のWebブラウザの永続cookiesは削除されない

結果、以下の文の評価結果が`true`となります。

```ruby:
elsif (user_id = cookies.signed[:user_id])
```

処理は以下の文まで進み…

```ruby:
if user && user.authenticated?(cookies[:remember_token])
```

Userモデルの`authenticated?`メソッドが呼び出され…

```ruby:User#authenticated?
def authenticated?(remember_token)
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
```

`BCrypt::Password.new(remember_digest)`メソッドでエラーとなります。「1つ目のWebブラウザにおける操作により、既に`remember_digest`の内容は`nil`である。パスワードのハッシュとして`nil`という値はありえないはずなのに、それを引数として`BCrypt::Password.new`メソッドが呼び出されてしまう。結果、例外を投げる」というわけです。

## テストによるバグの解決

ここは「テストによりバグを解決する」という局面です。実際には、以下のテストを書いていきます。

* 2番目のウィンドウでログアウトするユーザーを想定したテスト
* 記憶ダイジェストを持たないユーザーに対する動作のテスト

### 2番目のウィンドウでログアウトするユーザーを想定したテスト

#### テストを書く

```diff:
  require 'test_helper'

  class UsersLoginTest < ActionDispatch::IntegrationTest
    ...略

    test "login with valid information followed by logout" do
      get login_path
      post login_path, params: { session: { email: @user.email, password: 'password' } }
      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_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
```

#### テストを実行する（通らない）

テストを実行してみます。結果はエラーを返して終了です。

```
# rails test test/integration/users_login_test.rb
Running via Spring preloader in process 101
Started with run options --seed 8392

ERROR["test_login_with_valid_information_followed_by_logout", UsersLoginTest, 2.0612237000004825]
 test_login_with_valid_information_followed_by_logout#UsersLoginTest (2.06s)
NoMethodError:         NoMethodError: undefined method `forget' for nil:NilClass
            app/helpers/sessions_helper.rb:35:in `forget'
            app/helpers/sessions_helper.rb:42:in `log_out'
            app/controllers/sessions_controller.rb:18:in `destroy'
            test/integration/users_login_test.rb:33:in `block in <class:UsersLoginTest>'

  2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.18225s
2 tests, 13 assertions, 0 failures, 1 errors, 0 skips
```

「`forget`メソッドのレシーバーが`nil`であることに起因する`NoMethodError`エラー」というメッセージが出ていますね。実際に発生しているエラーを捕捉できています。

#### テストが通るようにする

Railsチュートリアル本文における解決策としては、「`log_out`は`logged_in?`が`true`の場合に限り呼び出すようにする」というものが挙げられています。

```diff:app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    ...略

    def destroy
-     log_out
+     log_out if logged_in?
      redirect_to root_url
    end
  end
```

再度テストを実行してみましょう。

```
# rails test test/integration/users_login_test.rb
Running via Spring preloader in process 114
Started with run options --seed 58185

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 2.00099s
2 tests, 16 assertions, 0 failures, 0 errors, 0 skips
```

無事テストが通りました。

### 記憶ダイジェストを持たないユーザーに対する動作のテスト

「複数のWebブラウザプログラムからのアクセスを想定した統合テストを書く」というのは、正直かなり困難です。しかしながら、「記憶ダイジェストを持たないユーザーを想定した統合テストを書く」というのは容易です。実際には、以下の手順でテストを進めていきます。

1. 記憶ダイジェストを持たないユーザーを用意する
2. 当該ユーザーに対して、`authenticated?`メソッドを実行する
3. `authenticated?`メソッドが`false`を返せばテスト成功

「記憶ダイジェストを持たないユーザー」というのは、`UserTest#setup`メソッドで定義する`@user`とは別に用意する必要があります。`@user`に対しては、`authenticated?`メソッドの実行結果が`true`になるからです。

#### テストを書く

実際のテストコードは以下の通りです。

```ruby:
test "authenticated? should return false for a new user with nil digest" do
  assert_not @user.authenticated?('')
end
```

`test/models/user_test.rb`にテストを追加していきます。

```diff:test/models/user_test.rb
  require 'test_helper'

  class UserTest < ActiveSupport::TestCase
    ...略
+
+   test "authenticated? should return false for a new user with nil digest" do
+     assert_not @user.authenticated?('')
+   end
  end
```

##### `authenticated?`の引数が空文字列である理由

`authenticated?`の引数が空文字列になっています。その理由を説明するために、ここで改めてUserモデルにおける`authenticated?`メソッドの実装を見てみましょう。

```ruby:User#authenticated?
def authenticated?(remember_token)
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end
```

`remember_digest`がダイジェストとして不正な文字列である場合、`is_password?`が呼び出される以前、`BCrypt::Password.new`の時点でエラーになります。今回実装するテストでは、このような場合に`@user.authenticated?('')`は`false`を返さなければならないという条件です。

`BCrypt::Password.new`が値を戻し、`is_password?`まで進めば、`is_password?`の実装上、戻り値は`true`か`false`になります。ゆえに、`authenticated?`の引数は空文字列でも何でも構わない、ということになります。

#### テストを実行する（通らない）

テストを実行してみます。結果はエラーを返して終了です。

```
# rails test test/models/user_test.rb
Running via Spring preloader in process 127
Started with run options --seed 13861

ERROR["test_authenticated?_should_return_false_for_a_new_user_with_nil_digest", UserTest, 0.17453030000069703]
 test_authenticated?_should_return_false_for_a_new_user_with_nil_digest#UserTest (0.18s)
BCrypt::Errors::InvalidHash:         BCrypt::Errors::InvalidHash: invalid hash
            app/models/user.rb:31:in `new'
            app/models/user.rb:31:in `authenticated?'
            test/models/user_test.rb:75:in `block in <class:UserTest>'

  12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.33746s
12 tests, 20 assertions, 0 failures, 1 errors, 0 skips
```

「`BCrypt::Errors::InvalidHash`エラー」というメッセージが出ていますね。実際に発生しているエラーを捕捉できています。

#### テストが通るようにする

Railsチュートリアル本文における解決策としては、「記憶ダイジェストが`nil`である場合、`authenticated?`メソッドが`false`を返すようにする」というものが挙げられています。

```diff: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
    ...略
  end
```

再度テストを実行してみましょう。

```
# rails test test/models/user_test.rb
Running via Spring preloader in process 140
Started with run options --seed 13423

  12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.27720s
12 tests, 21 assertions, 0 failures, 0 errors, 0 skips
```

無事テストが通りました。

# 演習 - ２つの目立たないバグ

## 1. [リスト 9.16](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-destroy_forget)で修正した行をコメントアウトし、２つのログイン済みのタブによるバグを実際に確かめてみましょう。まず片方のタブでログアウトし、その後、もう１つのタブで再度ログアウトを試してみてください。

```diff:app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    ...略
    def destroy
-     log_out if logged_in?
+     log_out# if logged_in?
      redirect_to root_url
    end
  end
```

上記変更を保存した上で、以下の操作を行います。

1. Firefoxの複数タブで、ログイン中のユーザーページを開く
2. 1つのタブで「Log out」をクリックし、ログアウト操作を行う
3. 別のタブで「Log out」をクリックする

`user`が`nil`であるのに`forget`メソッドを呼び出してしまい、結果`NoMethodError`が発生します。

<img width="1016" alt="スクリーンショット 2019-11-10 16.47.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/793fbbc5-f363-e841-e350-92e92b7af892.png">

`rails server`のログ画面には、以下のスタックトレースが表示されています。

    
    app/helpers/sessions_helper.rb:35:in `forget'
    app/helpers/sessions_helper.rb:42:in `log_out'
    app/controllers/sessions_controller.rb:18:in `destroy'
    

スクリーンショットの内容、ならびに`rails server`に表示されたトレースを見ると、`SessionsController#destroy`から`SessionsHelper#forget`へエラーの原因となるメソッド呼び出しが波及していっていることがわかります。

`test/integration/users_login_test.rb`の統合テストも通りません。

```
# rails test test/integration/users_login_test.rb
Running via Spring preloader in process 166
Started with run options --seed 55314

ERROR["test_login_with_valid_information_followed_by_logout", UsersLoginTest, 2.165016299999479]
 test_login_with_valid_information_followed_by_logout#UsersLoginTest (2.17s)
NoMethodError:         NoMethodError: undefined method `forget' for nil:NilClass
            app/helpers/sessions_helper.rb:35:in `forget'
            app/helpers/sessions_helper.rb:42:in `log_out'
            app/controllers/sessions_controller.rb:18:in `destroy'
            test/integration/users_login_test.rb:33:in `block in <class:UsersLoginTest>'

  2/2: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.19785s
2 tests, 13 assertions, 0 failures, 1 errors, 0 skips
```

## 2. [リスト 9.19](https://railstutorial.jp/chapters/advanced_login?version=5.1#code-authenticated_p_fixed)で修正した行をコメントアウトし、２つのログイン済みのブラウザによるバグを実際に確かめてみましょう。まず片方のブラウザでログアウトし、もう一方のブラウザを再起動してサンプルアプリケーションにアクセスしてみてください。

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

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

    ...略
  end
```

上記変更を保存した上で、以下の操作を行います。

1. FirefoxとSafariで、特定のユーザーにログインする
2. Firefoxで「Log out」をクリックし、ログアウト操作を行う
3. Safariで、ログアウトせずに終了する
4. Safariを再起動し、アプリケーションの任意のページを表示する

Safariで`BCrypt::Errors::InvalidHash`エラーが発生します。

<img width="1245" alt="スクリーンショット 2019-11-10 16.34.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/8088c0fb-2b58-7e44-abf2-9861f59b95b8.png">

`rails server`のログ画面には、以下のスタックトレースが表示されています。

    
    app/models/user.rb:32:in `new'
    app/models/user.rb:32:in `authenticated?'
    app/helpers/sessions_helper.rb:21:in `current_user'
    app/helpers/sessions_helper.rb:30:in `logged_in?'
    

スクリーンショットの内容、ならびに`rails server`に表示されたトレースを見ると、`BCrypt::Password.new(remember_digest)`でエラーが発生していることがわかります。

```ruby:User#authenticated?
def authenticated?(remember_token)
  # return false if remember_digest.nil?
  BCrypt::Password.new(remember_digest).is_password?(remember_token) # エラー
end
```

`test/models/user_test.rb`の統合テストも通りません。

```
# rails test test/models/user_test.rb
Running via Spring preloader in process 153
Started with run options --seed 35463

ERROR["test_authenticated?_should_return_false_for_a_new_user_with_nil_digest", UserTest, 0.3806449999992765]
 test_authenticated?_should_return_false_for_a_new_user_with_nil_digest#UserTest (0.38s)
BCrypt::Errors::InvalidHash:         BCrypt::Errors::InvalidHash: invalid hash
            app/models/user.rb:32:in `new'
            app/models/user.rb:32:in `authenticated?'
            test/models/user_test.rb:75:in `block in <class:UserTest>'

  12/12: [=================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.41116s
12 tests, 20 assertions, 0 failures, 1 errors, 0 skips
```

## 3. 上のコードでコメントアウトした部分を元に戻し、テストスイートが`red`から`green`になることを確認しましょう。

コードを以下のように変更します。

```diff:app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    ...略
    def destroy
-     log_out# if logged_in?
+     log_out if logged_in?
      redirect_to root_url
    end
  end
```

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

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

    ...略
  end
```

アプリケーション全体に対するテストを実行します。

```
# rails test                              
Running via Spring preloader in process 179
Started with run options --seed 8193

  25/25: [=================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.48940s
25 tests, 69 assertions, 0 failures, 0 errors, 0 skips
```

無事テストが通りました。