Ruby on Rails チュートリアル第9章をやってみて

#発展的なログイン機構
■第9章
`remember me`というブラウザを再起動した後でもすぐにログインできる機能を備えていることが一般的になってきた。
永続クッキー (permanent cookies) を使ってこの機能を実現していく。

##9.1 Remember me 機能
本節では、ユーザーのログイン状態をブラウザを閉じた後でも有効にする [remember me] 機能を実装していく。この機能を使えば、ログアウトを実行しない限りログイン状態を維持できる。
YahooとかTwitterみたいな感じなのかな？

###9.1.1 記憶トークンと暗号化
セッションの永続化の第一歩として記憶トークン (remember token) を生成し、`cookies`メソッドによる永続的cookiesの作成や、安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用。


`cookies`メソッドに保存する情報は自動的に安全に保たれるようになっていない。cookiesを永続化するとセッションハイジャックという攻撃を受ける可能性がある。

次の方針で永続的セッションを作成する。

・記憶トークンにはランダムな文字列を生成して用いる。

・ブラウザのcookiesにトークンを保存するときには、有効期限を設定する。

・トークンはハッシュ値に変換してからデータベースに保存する。

・ブラウザのcookiesに保存するユーザーIDは暗号化しておく。

・永続ユーザーIDを含むcookiesを受け取ったら、そのIDでデータベースを検索し、記憶トークンのcookiesがデータベース内のハッシュ値と一致することを確認する。

記憶トークンとして、Ruby標準ライブラリの`SecureRandom`モジュールにある`urlsafe_base64`メソッドを使う。このメソッドは長さ22のランダムな文字列を返す。

rails consoleを立ち上げて使ってみるとこんな感じ。

```rb
SecureRandom.urlsafe_base64
=> "XkxMplhn38qjFYlFMA3KiQ"
```

ユーザーを記憶するために、記憶トークンを作成して、そのトークンをダイジェストに変換したものをデータベースに保存。

新しいトークンを作成するための`new_token`メソッドを作成。また、記憶トークンをユーザーと関連付け、トークンに対応する記憶ダイジェストをデータベースに保存する`user.remember`メソッドも作成。

```ruby:user.rb
class User < ApplicationRecord
  attr_accessor :remember_token 
.
.
.
.
 # ランダムなトークンを返す
  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
```

###9.1.2 ログイン状態の保持
個別のcookiesは、1つの`value `(値) と、オプションの`expires` (有効期限) からできている。

記憶トークンと記憶ダイジェストを比較する`authenticated?`メソッドを、Userモデルの中に置く。

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

その後もいろんなファイルの中身の書き換え。だいぶ何言ってるかわからなくなってきました。。。

###9.1.3 ユーザーを忘れる
ユーザーを忘れるためのメソッドを定義する。
この`user.forget`メソッドによって、`user.remember`が取り消される。
以下を追記。

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

永続セッションを終了するために、`forget`ヘルパーメソッドを追加して`log_out`ヘルパーメソッドから呼び出す。

```ruby:sessions_helper.rb
# 永続的セッションを破棄する
  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
```

###9.1.4 2つの目立たないバグ
バグが2つある
1つ目は、2つタブを開いてる状態でログアウトを2回押したときにアクセスエラーになる。この問題の回避のため、ユーザーがログイン中の場合にのみログアウトさせる。

2つ目は、2つのブラウザでログイン状態で、片方をログアウトしてもう片方はログアウトせずに再起動するとややこしくなる。

1つ目のは統合テストにコードを追記して、コントローラでログイン中の場合のみログアウトするようにする。


```ruby:sessions_controller.rb
 def destroy
    log_out if logged_in?
    redirect_to root_url
  end
```

2番目の問題についてですが、統合テストで2種類のブラウザをシミュレートするのはかなり困難らしい。一応コードは書き換えたものの、ここらへんも解説がイマイチ理解できませんでしたね。。。

##9.2 [Remember me] チェックボックス
ログインフォームにチェックボックスを追加するところから始める。ヘルパーメソッドで作成できる。正常に動作するためにはラベルの内側に配置する必要ある。

```new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>
```
`check`と`inline`はCSSクラス。

ログインフォームの編集が終わったので、チェックボックスがオンのときにユーザーを記憶し、オフのときには記憶しないようにする。

```rb
params[:session][:remember_me]
```

でチェックボックスのONOFFを取り出せる。

##9.3[Remember me] のテスト
動作をテストで確認できるようにしておくことが重要。

###9.3.1 [Remember me] ボックスをテストする
テスト内でユーザーがログインできるようにする。

`log_in_as`というヘルパーメソッドを作成。

```rb
def log_in_as(user)
  session[:user_id] = user.id
end
```

統合テストでも同様のヘルパーを実装。

```rb
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
```

```rb
cookies[:remember_token]
```

これだと常に`nil`となるので、文字列をキーに返すようにする。

```rb
cookies['remember_token']
```

###9.3.2 [Remember me] をテストする
`current_user`内のある複雑な分岐処理については、テストが行われていない。`current_user`をリファクタリングするのであれば同時にテストも作成しておくことが重要らしい。

以下を作る。

```rb
$ touch test/helpers/sessions_helper_test.rb
```

テスト手順は

・fixtureで`user`変数を定義する

・渡されたユーザーを`remember`メソッドで記憶する

・`current_user`が、渡されたユーザーと同じであることを確認する

永続的セッションのテスト

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

`assert_equal`の引数は「期待する値, 実際の値」の順序で書く。

##感想
今回はログイン機能を拡張しました。やっぱり章が進むほど、何言ってるかわかんなくなってくるので、要勉強だなとつくづく思います。。




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