Rails 5.1 API mode + webpacker + react + reactstrap な ToDO アプリに Twitter OAuth 認証を追加する(Sorcery gem)

# 関連記事

こちらの記事の続きです

* [Rails 5.1 API mode + webpacker + react + reactstrap で ToDO アプリを書く](https://qiita.com/kaishuu0123/items/00b89e092f156a02a3e5)
* [Rails 5.1 API mode + webpacker + react + reactstrap な ToDO アプリに認証機能を追加する (sorcery gem で JWT)](https://qiita.com/kaishuu0123/items/27ba55e774ac1b7a94fc)

# 導入手順

* 基本的な手順は公式に記載されているので、その手順に従って導入を進めます
* https://github.com/Sorcery/sorcery/wiki/External

ドキュメントに記載されていないところで、重要なポイントは **Rails API mode でプロジェクトを作成した場合**には、<font color="red">Cookie と Session Cookie が有効になっていません</font> ([公式へのリンク](https://railsguides.jp/api_app.html#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2))
Rails を使った方なら分かると思いますが、具体的に言えば `session` が使えない、ということになります

ですので、 `config/application.rb` で `ActionDispatch::Session::CookieStore` を有効にする必要があります。(もちろんですが、本番環境などでは MemcacheStore などを使うようにしましょう)

```ruby:config/application.rb
     # Only loads a smaller set of middleware suitable for API only apps.
     # Middleware like session, flash, cookies can be added back manually.
     # Skip views, helpers and assets when generating a new resource.
     config.api_only = true

+    config.middleware.use ActionDispatch::Cookies
+    config.middleware.use ActionDispatch::Session::CookieStore
```

# React アプリとの連携

* 意外と悩ましいこととして、認証完了後にどのようにして React アプリ側に JWT トークンを渡すのか、という点があります
* 大雑把に認証の流れを整理すると、以下のようになると思います
    1. OAuth 認証を行うボタンをクリック
    2. Twitter のアプリ連携確認画面に遷移
    3. アプリ連携を twitter の画面から許可
    4. Rails サーバ側にリダイレクトされる
    5. Rails サーバで認証処理を行う
    6. ...
* 整理してみると、上記 6. のタイミングでいい感じに React アプリに JWT トークンが渡せればいい、ということになります
* [Spring Security REST の OAuth](http://alvarosanchez.github.io/grails-spring-security-rest/latest/docs/index.html#oauth) で実現している方法を ~~パクって~~ 参考にして、Redirect 時に JWT Token を URL パラメータとして React アプリ側に渡すようにします

サーバ側のコードに示すと以下のようになります。 `def callback` が twitter のアプリ連携確認画面から返ってきたときのアクションです

```ruby:app/controllers/oauths_controller.rb
class OauthsController < ApplicationController
  def oauth
    login_at(params[:provider])
  end

  def callback
    provider = params[:provider]
    if @user = login_from(provider)
      # ここで JWT token 発行
      tokens = Jwt::TokenProvider.refresh_tokens @user
      # React アプリを動かしている URL にリダイレクトする
      # http://localhost:3000/#/?access_token=....&refresh_token=... という形でリダイレクトさせる
      redirect_to "#{File.join(root_url, '#', '?')}#{tokens.to_query}", notice: "Logged in from #{provider.titleize}"
    else
      begin
        @user = create_from(provider)

        reset_session
        auto_login(@user)
        # ここで JWT token 発行
        tokens = Jwt::TokenProvider.refresh_tokens @user
        # React アプリを動かしている URL にリダイレクトする
        # http://localhost:3000/#/?access_token=....&refresh_token=... という形でリダイレクトさせる
        redirect_to "#{File.join(root_url, '#', '?')}#{tokens.to_query}", :notice => "Logged in from #{provider.titleize}"
      rescue
        redirect_to root_path, :alert => "Failed to login from #{provider.titleize}"
      end
    end
  end
end
```

# React アプリ側で params を受け取る

* こちらは比較的簡単です
* React Router v4 を使っている場合、 `location.search` で取得可能ですので、 `componentDidMount` のタイミングで取得するようにします
* コードの一部を記載すると、以下のようになります

```javascript
...
    let tokens = queryString.parse(get(this, "props.location.search"));
    if (!isEmpty(tokens)) {
      localStorage.setItem('access_token', tokens.access_token)
    }
...
```

`isEmpty`、`get` はそれぞれ、`lodash/isEmpty`、`lodash/get` を利用しています

# 最後に
* 意外と簡単に OAuth 認証を実現することができました
    * 話題が逸れますが、 「OAuth 認証」という単語は微妙らしく、 [こんな記事](https://qiita.com/TakahikoKawasaki/items/f2a0d25a4f05790b3baa) もあるんですね
* URL パラメータに JWT トークンを渡すのはセキュリティ的にどうなんだろう、と疑問に思って調べてみたところ、こんな記事がありました。
    * [HTTPS通信時のURLは暗号化されるか](http://hogem.hatenablog.com/entry/20100307/1267977441)