OmniAuth(OAuth2)処理をキックしたときのパラメータを認証用リクエストにも渡したい

# OmniAuth(OAuth2)処理をキックしたときのパラメータを認証用リクエストにも渡したい

## はじめに
RailsアプリにOAuth2認証を導入する場合に利用できるOmniAuthがあります。
TwitterやGitHubなどの外部認証用にすでにいくつも[Strategy](https://github.com/omniauth/omniauth/wiki/List-of-Strategies)が用意されています。

リストにない場合は自作することになりますが、認証サービス用のStrategyを作成時にはまった点を紹介します。

特にOmniAuth処理をキックされたときの動的なパラメータを扱う点は、検索しても情報が見つけられなかったので苦労しました。
参考になれば幸いです。

## ここで取り上げるポイント
- OAuth用のURLを変更する
- callback_urlの注意点（デフォルトのままだとエラーになってしまうパターン）
- **OmniAuth処理をキックされたときのパラメータを認証時のパラメータとして渡す**
- **OmniAuth処理をキックされたときのパラメータをapi呼び出し結果に格納する**

OAuth2の処理の流れはこちらをご覧ください。（とてもわかりやすかったです）
[「OmniAuth OAuth2 を使って OAuth2 のストラテジーを作るときに知っていると幸せになれるかもしれないこと」](http://qiita.com/tyamagu2/items/d242b6dd4a20b657bb39)

## OmniAuthでOAuth2用のカスタムStrategyを作成

### 前提
今回は架空のhogeというOAuth2認証サービス用のstrategyを作成します。

#### API側

||例|
|-----|-----|
| サイト | `https://hoge.xxx.xxx` |
| 認可エンドポイント | `https://hoge.xxx.xxx/oauth/authorize` |
| 認可時に渡すカスタムパラメータ | group_id=9876543210 |
| api利用用に登録したRedirect URL | `https://myapp.localhost/sessions/callback` |
| 呼び出すapi | `https://hoge.xxx.xxx/api/user_information` |
| apiの戻り値 | json形式※1 |

```json:(※1)apiの戻り値
{
  "user_id" : "012345",
  "first_name" : "Firstname",
  "last_name" : "Lastname",
  "email" : "test@localhost"
}
```

#### OAuthを利用するWebアプリ側
||例|
|-----|-----|
| サイト | `https://myapp.localhost` |
| OAuth用リクエストパス | `https://myapp.localhost/sessions/auth` |
| OAuth用コールバックパス | `https://myapp.localhost/sessions/callback` |

OAuth認証時に呼び出すリクエストパスと、OAuth認証結果を受け取るコールバックパスはデフォルトのパスから変更しています。

| 変更前（デフォルト） | 変更後 |
|-----|-----|
| `https://myapp.localhost/auth/hoge` | `https://myapp.localhost/sessions/auth` |
| `https://myapp.localhost/auth/hoge/callback` | `https://myapp.localhost/sessions/callback` |

### 1. カスタムStrategyクラスを作成する

`lib/omniauth/strategies/hoge.rb`

を作成します。

https://github.com/intridea/omniauth-oauth2 などをひな形にして次のようなファイルを作成します。

```rb:lib/omniauth/strategies/hoge.rb
require 'omniauth-oauth2'
module OmniAuth
  module Strategies
    class Hoge < OmniAuth::Strategies::OAuth2
      # Give your strategy a name.
      option :name, "hoge"

      # This is where you pass the options you would pass when
      # initializing your consumer from the OAuth gem.
      option :client_options, { :site => 'https://myapp.localhost' }

      # These are called after authentication has succeeded. If
      # possible, you should try to set the UID without making
      # additional calls (if the user id is returned with the token
      # or as a URI parameter). This may not be possible with all
      # providers.
      uid{ raw_info['user_id'] }

      info do
        {
          # JSON結果を加工しておける
          user_id: raw_info['user_id'],
          first_name: raw_info['first_name'],
          last_name: raw_info['last_name'],
          email: raw_info['email'],
          # リクエストパラメータも取得できる
          group_id: request.env['omniauth.params']['group_id']
        }
      end

      extra do
        {
          'raw_info' => raw_info
        }
      end

      def raw_info
        @raw_info ||= JSON.load(access_token.get('/api/user_information').body)
      end
      
      # リクエストにカスタムパラメータを追加
      def request_phase
        _authorize_params = { :redirect_uri => callback_url }.merge(authorize_params)
        _authorize_params.merge!({ group_id: request.env['omniauth.params']['group_id'] })
        redirect client.auth_code.authorize_url(_authorize_params)
      end
      
      # リダイレクトURLを固定（デフォルトではクエリパラメータが付加されるため）
      def callback_url
        full_host + script_name + callback_path
      end
    end
  end
end
```

以下、それぞれの説明です。

#### 1.1 callback_urlの注意点

```rb
def callback_url
  full_host + script_name + callback_path
end
```

ここでcallback_urlをオーバーライドしていますが、理由があります。

callback_urlはデフォルトでクエリパラメータが付加されるため、`OmniAuth2::Client#request` でエラーになる可能性があります。（登録されているアプリと違うと判断されるため）

```rb:original
def callback_url
  full_host + script_name + callback_path + query_string
end
```

Hoge#callback_urlでquery_stringが付加されない形に上書きしています。

#### 1.2 OAuth認証時に動的にベンダー用カスタムパラメータを渡す

```rb
def request_phase
  _authorize_params = {:redirect_uri => callback_url}.merge(authorize_params)
  _authorize_params.merge!({ group_id: request.env['omniauth.params']['group_id'] })
  redirect client.auth_code.authorize_url(_authorize_params)
end
```

`https://hoge.xxx.xxx/oauth/authorize?group_id=9876543210`のようにOAuth認証にパラメータを追加したい場合の対応です。

固定値を渡す場合は、

```rb:lib/omniauth/strategies/hoge.rb
:
option :authorize_params, { group_id: '9876543210' }
:
```

のように書いておけば良いのですが、動的に値を変えたい場合はどうすれば良いのでしょう？
このパラメータ(group_id)を、OAuth認証をキックするときに受け取るようにします。

`https://myapp.localhost/sessions/auth?group_id=9876543210`

OmniAuthでは、`/sessions/auth`呼び出し時のパラメータが
`request.env['omniauth.params']`
に格納されています。

今回の例では、`request.env['omniauth.params']['group_id']`となります。
このパラメータをOAuthリクエストパラメータに追加すればいいわけです。

パラメータを追加すべき処理は
`OmniAuth::Strategies::OAuth2#request_phase`
になります。

```rb:original
def request_phase
  redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(authorize_params))
end
```

Hoge#request_phaseでパラメータを追加する形で上書きします。

この変更により
`https://hoge.xxx.xxx/oauth/authorize?group_id=9876543210`
のようにパラメータが渡されます。

#### 1.3 api呼び出し結果を取得する

```rb
def raw_info
  @raw_info ||= JSON.load(access_token.get('/api/user_information').body)
end
```

認可されたaccess_tokenを使ってapiを呼び出します。
今回は結果がjson形式で受け取れることを想定しているので、parseして@raw_infoに格納します。

#### 1.4 結果をHashにセットする
```rb
uid{ raw_info['user_id'] }

info do
  {
    # JSON結果を加工しておける
    user_id: raw_info['user_id'],
    first_name: raw_info['first_name'],
    last_name: raw_info['last_name'],
    email: raw_info['email'],
    # リクエストパラメータも取得できる
    group_id: request.env['omniauth.params']['group_id']
  }
end

extra do
  {
    'raw_info' => raw_info
  }
end
```

OmniAuthによるapi呼び出し結果(OmniAuth::AuthHash)は`request.env['omniauth.auth']`に格納されます。
次のように結果にアクセスできるので、その値をそれぞれ設定しています。

```rb
request.env['omniauth.auth'].uid
request.env['omniauth.auth'].info
request.env['omniauth.auth'].extra
request.env['omniauth.auth'].credentials # tokenに関する情報(今回上書きしてません)
```

このHashにはOmniAuth呼び出し時のパラメータも設定できます。

```rb
group_id: request.env['omniauth.params']['group_id']
```

### 2. HogeクラスをOmniAuthのProviderに設定する

OmniAuthがHogeを利用できるように `config/initializers/omniauth.rb` を作成します。

```rb:config/initializers/omniauth.rb
require 'omniauth/strategies/hoge'

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :hoge, ENV['KEY'], ENV['SECRET'],
    :request_path => '/sessions/auth', :callback_path => '/sessions/callback'
end
```

#### 2.1 request_pathとcallback_pathを変更する

```rb
  provider :hoge, ENV['KEY'], ENV['SECRET'],
    :request_path => '/sessions/auth', :callback_path => '/sessions/callback'
```

namespaceを使っている場合など、認証用のパスと戻り値を受け取るパスを変更したい場合があります。
今回の設定では次のようにパスを変更しています。

|対象| 変更前(デフォルト) | 変更後 |
|-----|-----|-----|
|requrest_path|`/auth/hoge`|`/sessions/auth`|
|callback_path|`/auth/hoge/callback`|`/sessions/callback`|

### 3. 認証結果を元にユーザーを作成する

今回は`SessionsController#create`で認証結果を受け取ります。

```rb:app/controllers/sessions_controller.rb
:
def create
  user = User.find_or_create_from_auth_hash(auth_hash)
  :
end

private

def auth_hash
  request.env['omniauth.auth'].info
end
:
```

先程説明したように、apiの呼び出し結果は`request.env['omniauth.auth']`で取得できます。
必要な情報はinfoで取得できるようにしてあるので、`request.env['omniauth.auth'].info`の値を利用してUserを作成します。

```rb:app/models/user.rb
:
def self.find_or_create_from_auth_hash(auth_information)
  user = User.find_or_initialize_by(user_id: auth_information[:user_id])
  user.email = auth_information[:email]
  :
  user.save!
  user
end
:
```

### 最後に

OAuth2について詳しく知りたい場合は、

- OmniAuth::Strategy
- OmniAuth::Strategies::OAuth2

のコードを読むと良いかと思います。（いろいろ見えてきます）

### 参考
- [intridea/omniauth-oauth2](https://github.com/intridea/omniauth-oauth2)
- [OmniAuth OAuth2 を使って OAuth2 のストラテジーを作るときに知っていると幸せになれるかもしれないこと](http://qiita.com/tyamagu2/items/d242b6dd4a20b657bb39)
- [OmniAuthで認証やコールバックのURLを変える方法](http://d.hatena.ne.jp/tkawa/20111014/p1)
- [OAuth2 に対応した API の OmniAuth Strategy を書いてみる](http://qiita.com/dany1468/items/ee5cfda1716705937c30)
- [Google OAuth 2 redirect_uri_mismatchになる](http://haikikyou.hatenablog.com/entry/2016/01/09/012859)