OmniAuth OAuth2 を使って OAuth2 のストラテジーを作るときに知っていると幸せになれるかもしれないこと

＜この記事は「[Money Forward Advent Calendar 2015](http://qiita.com/advent-calendar/2015/moneyforward)」の20日目の記事です＞

最近 OmniAuth 用の OAuth2 のストラテジーを作る機会が何度かあったので、gem のコードやドキュメントを読んで調べたことをまとめました。独自に OmniAuth のストラテジー、特に OAuth2 のストラテジーを作る場合は是非参考にしてください。

なお、本投稿は [OmniAuth の Strategy Contribution Guide](https://github.com/intridea/omniauth/wiki/Strategy-Contribution-Guide) および [OmniAuth](https://github.com/intridea/omniauth)、[OmniAuth OAuth2](https://github.com/intridea/omniauth-oauth2)、さらに [有志が開発した OmniAuth 用の各種ストラテジー](https://github.com/intridea/omniauth/wiki/List-of-Strategies)の実装を参考にしています。

# OmniAuth

[OmniAuth](https://github.com/intridea/omniauth) は、認証プロバイダを利用してユーザ認証する方法を標準化するための gem です。

例えば、Web アプリケーションを作るときに、複数の認証プロバイダを利用してユーザ認証できるようするとします。その時に、こっちのプロバイダは OAuth2 を採用していて、あっちは LDAP で、となったら、色々なプロトコルに対応しないと行けなくて大変ですね。また、同じ OAuth2 でも、プロバイダによってはプロトコルで定められた範囲外での要求事項があるかもしれません。

OmniAuth は、ストラテジーと呼ばれる仕組みを提供し、認証プロバイダを介したユーザ認証方法を標準化しています。どんな認証プロバイダを使う時でも、そのプロバイダに対応する Omniauth のストラテジーが用意されていれば、大体同じようなインターフェースを介してユーザ認証ができるのです。素晴らしいですね。

## OmniAuth のストラテジー

OmniAuth のストラテジーは Rack のミドルウェアです。Rails アプリで omniauth のストラテジーを使ったことがある人は、以下の様な記述をしたことがあるかもしれません。

```config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  # omniauth-twitter のストラテジを登録
  provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
end
```

`Omniauth::Builder#provider` の中身を見ると、[実際にミドルウェアを登録しているのがわかります](https://github.com/intridea/omniauth/blob/v1.3.1/lib/omniauth/builder.rb#L47-L59)。

また、上記のように omniauth-twitter ストラテジを登録した状態で `rake middleware` を実行して見ると、ミドルウェア一覧に omniauth-twitter のストラテジが出てきます。

```
> rake middleware
...
use OmniAuth::Strategies::Twitter
...
```

ストラテジーは Rack のミドルウェアなので、rack アプリケーションであれば、rails に限らず、例えば sinatra などでも利用可能です。

## ストラテジーを使った認証の流れ

ストラテジーを使った認証は、主に以下の３つのフェーズに分かれています。

### セットアップフェーズ

ストラテジーの設定を行うフェーズです。動的なセットアップが必要な場合はここで行います。例えば、事前に必要な外部への http リクエストを行ったり、動的に変えたいオプションの値を作成してセットしたりします。不要な場合は特に何もしなくて構いません。

セットアップフェーズの実装は以下のようになっています。`setup` オプションに Rack エンドポイントを渡せばそいつを呼び出してくれます。また truthy な値が渡されていれば、OmniAuth を使っているアプリのセットアップ用のパス（デフォルトでは`/auth/:provider/setup`）が呼び出されます。デフォルトでは false なのでセットアップフェーズは呼び出されません。

```lib/omniauth/strategy.rb
def setup_phase
  if options[:setup].respond_to?(:call)
    log :info, 'Setup endpoint detected, running now.'
    options[:setup].call(env)
  elsif options.setup?
    log :info, 'Calling through to underlying application for setup.'
    setup_env = env.merge('PATH_INFO' => setup_path, 'REQUEST_METHOD' => 'GET')
    call_app!(setup_env)
  end
end
```

また、単純に setup_phase をオーバーライドしても良さそうです。

### リクエストフェーズ

リクエストフェーズでは、認証に関する情報の収集を行います。例えば、OAuth2 などでは外部サイトへのリダイレクトを行い、LDAP などではユーザに情報の入力をさせたりするフェーズです。

独自のストラテジーを作成する場合は、`request_phase` メソッドを上書きしてリクエストフェーズの実装を行います。

### コールバック

コールバックフェーズでは、リクエストフェーズで集めたデータを、OmniAuth が定めたフォーマットに詰め込んで、アプリケーションに渡します。

独自のストラテジーを作成する場合、`callback_phase` メソッドを上書きしても良いのですが、OmniAuth が用意している DSL を使えば、適切なフォーマットにデータをセットする作業を OmniAuth に任せる事ができます。例えばこんな感じです。

```rb
module OmniAuth
  module Strategies
    class Developer
      include OmniAuth::Strategy
      ...
      uid do
        request.params[options.uid_field.to_s]
      end
    end
  end
end
```

DSL は以下の通りです。

- uid: プロバイダ内でユニークな ID
- info: ユーザに関する様々な情報を持つハッシュ。キーは [Auth Hash Schema](https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema) に記載されているものが利用できます。
- credentials: ストラテジーの実行によって何かしらのクレデンシャルを取得する場合はここにセットします。
- extra: 追加で含めておきたい情報をここに含めます。

## ストラテジーの設定

ストラテジーの設定は、OmniAuth が提供する Options オブジェクトにセットすることが強く推奨されています。オプションを宣言してデフォルト値をセットするには、以下の用に `option オプション名, デフォルト値` と記述します。

```rb
module OmniAuth
  module Strategies
    class Developer
      include OmniAuth::Strategy

      option :fields, [:name, :email]
      option :uid_field, :email
    end
  end
end
```

このように宣言したオプションは、ストラテジーの初期化時に自動でデフォルト値に設定されます。また、初期化時に上書きすることも可能です。

```rb
app = lambda{|env| [200, {}, ["Hello World."]]}
OmniAuth::Strategies::Developer.new(app).options.uid_field                      # => :email
OmniAuth::Strategies::Developer.new(app, :uid_field => :name).options.uid_field # => :name
```

provider メソッドを使ってストラテジーを登録する場合も、[最後の引数にハッシュを渡すことでオプションの上書きが可能](https://github.com/intridea/omniauth/blob/v1.3.1/lib/omniauth/builder.rb#L58-L59)です。

```config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'], scope: [:email]
end
```

# Omniauth OAuth2

[Omniauth OAuth2](https://github.com/intridea/omniauth-oauth2) は、Omniauth 用の OAuth2 ストラテジーの雛形を提供する gem です。この gem が定義する `OmniAuth::Strategies::OAuth2` を継承して独自のストラテジーを作れば、簡単に OAuth2 に対応することができます。

## OmniAuth OAuth2 ストラテジーの各フェーズ

### リクエストフェーズ

リクエストフェーズでは外部サービスにリダイレクトするだけです。

```lib/omniauth/strategies/oauth2.rb
def request_phase
  redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(authorize_params))
end
```

ここで使われている client は、[OAuth2 gem](https://github.com/intridea/oauth2) が提供する `OAuth2::Client` クラスのインスタンスです。`client_id` と `client_secret` 以外に追加でオプションを渡す場合は、`client_options` オプションにハッシュを渡します。

```lib/omniauth/strategies/oauth2.rb
def client
  ::OAuth2::Client.new(options.client_id, options.client_secret, deep_symbolize(options.client_options))
end
```

#### redirect_uri
redirect_uri は外部サービスがユーザを認証＆認可を確認した後に、ユーザエージェントをリダイレクトしてくる先です。

callback_url は OmniAuth に以下のように定義されています。

```lib/omniauth/strategy.rb
def callback_url
  full_host + script_name + callback_path + query_string
end
```

実は、callback_url は OmniAuth OAuth2 で以下のようにオーバーライドされていましたが、[つい最近の PR で消されています](https://github.com/intridea/omniauth-oauth2/pull/70)。これによって `redirect_uri` に渡す URL にクエリ文字列まで含まれるようになったのですが、これによって登録済みの redirect_uri とストラテジーが渡す redirect_uri が一致しなくなるケースがあり、結構色んなストラテジーが動かなくなったようです。[背景はこの辺のコメント](https://github.com/intridea/omniauth-oauth2/issues/81#issuecomment-151281907)に書いてありますが、新しくストラテジーを作る場合は注意したいところです。

```lib/omniauth/strategies/oauth2.rb
def callback_url
  full_host + script_name + callback_path
end
```

#### authorize_params
`#authorize_params` が返すハッシュには任意のパラメータを追加することができます。そのようなパラメータをオプションとして渡す場合は、`authorize_options` オプションの配列に、渡したいオプションのキーを含めます。デフォルトでは、`authorize_options` は `:scope` のみを含む配列です（`option :authorize_options, [:scope]`)。つまり、`scope`  オプションに値を渡しておけば、`#authorize_params` はデフォルトで `{ scope: 'xxx' }` のようなハッシュを返します。

また、`#authorize_params` 内では `state` パラメータもセットしています。この `state` パラメータは [RFC にも定められている通り](https://tools.ietf.org/html/rfc6749#section-4.1.1)、クライアントが渡した値がそのまま外部サービス（認可サーバ）から返ってきます。OmniAuth OAuth2 ストラテジーでは、state の値をセッションにも保存しておいて、コールバックフェーズで外部サービスから返ってきた値と比較することで CSRF 対策を行っています。

### コールバックフェーズ

コールバックフェーズでは、前述の state のチェックや各種エラー処理などを行った後、アクセストークンを取得します。メインの処理は以下のところです。ここでも `OAuth2::Client` のインスタンスを使ってトークンを取得しています。

```lib/omniauth/strategies/oauth2.rb
def build_access_token
  verifier = request.params["code"]
  client.auth_code.get_token(verifier, {:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)), deep_symbolize(options.auth_token_params))
end
```

`#token_params` が返すハッシュにパラメータを追加する場合は、authorize_params の時と同様に、含めたいパラメータをセットしたオプションのキーを `token_options` の配列に追加しておきます。

## アプリケーションに返す情報の作成

アクセストークンやリフレッシュトークンなどはデフォルトで以下の通りセットしてくれているのであまり気にする必要はありません。

```lib/omniauth/strategies/oauth2.rb
credentials do
  hash = {"token" => access_token.token}
  hash.merge!("refresh_token" => access_token.refresh_token) if access_token.expires? && access_token.refresh_token
  hash.merge!("expires_at" => access_token.expires_at) if access_token.expires?
  hash.merge!("expires" => access_token.expires?)
  hash
end
```

`info` を設定するには追加の API 呼び出しが必要なことが多いと思われますが、ちょうど取得してきたアクセストークンがあるので、`OAuth2::Client` を使えば簡単に取得できそうですね。ちなみに `info` の取得は `skip_info` オプションによって無効にできます。

`uid` については、可能な限り追加の API 呼び出し無しでセットすることが推奨されています。外部サービスからのコールバック時のパラメータなどに `uid` が含まれている場合はそちらを使いましょう。

# TIPS など

## HTTP 通信したい/JSON パースしたい/XML パースしたい
上記のことをやる場合は、以下の gem を使うことが強く推奨されています。特別な理由がない限りは、上記の目的でこれら以外の gem は使わないようにしましょう。
- [faraday](https://github.com/lostisland/faraday_
- [multi_json](https://github.com/lostisland/faraday)
- [multi_xml](https://github.com/sferik/multi_xml)

## 特殊なストラテジー名にしたい
例えば `oauth` を単純にクラス名にしようとすると `Oauth` になります。これを `OAuth` にしたい、という時は、`OmniAuth.config.add_camelization('oauth', 'OAuth')` のように `add_camelization` を使います。

また、`google_oauth2` のように gem 名（ファイル名）の途中にアンダースコアを含んだりする場合は、`name` オプションによりプロバイダー名と手動で対応しておかないと上手く動かなかったりします。 こんな感じです。 `option :name, 'google_oauth2'`

## 同じストラテジーを複数回登録したい

`name` オプションを使うと同じストラテジーを複数回登録できます。

```
provider :twitter, CLIENT_ID1, CLIENT_SECRET1
provider :twitter, CLIENT_ID2, CLIENT_SECRET2, name: 'twitter2'
```

## ssl の認証を無効にしたい
OAuth2::Client を使って外部と通信するときに、どうしても開発環境で SSL の認証を無効にしたい時があったとします。そんな場合は `client_options: { ssl: { verify: false } }` のように、client_options を渡してあげると良いです。ただし本番では絶対にやらないように。

## テスト用に通信をモックしたい

[この辺](https://github.com/intridea/omniauth/wiki/Integration-Testing)を参考にしてください。

---

なんか取り留めのない感じになってしまいましたが、参考になれば幸いです。なにか間違いやわかりにくい点があればご連絡ください。

実際にストラテジーを作る場合は、パッケージの構成なども指定されています。その他参考になる情報が色々と書いてあるので、[Strategy Contribution Guide]https://github.com/intridea/omniauth/wiki/Strategy-Contribution-Guide) は必読です。ここに書いたことも、このガイドを参考にして、更にコードを読みつつ試しつつ調べたものです。

それではみなさん、良い OmniAuth ライフを。