Phoenix で OAuth 認証をする

Phoenix で OAuth 認証をやってみます。
今回は [GitHub の OAuth](https://developer.github.com/v3/oauth/) を使いますが、Twitter や Facebook といったものにもつかえると思います。

また、認証用ライブラリは[コチラ](https://github.com/scrogson/oauth2)を利用します。実装はこちらに公開されているサンプルを追っていくカタチとなります。

Phoenix アプリケーションの作成については[コチラ](http://qiita.com/mserizawa/items/a861b4aa0d55b42b2324)をご参照ください。

# 下準備: GitHub アプリケーションを登録する

まずは GitHub にアプリケーションを登録して、各種キーを手に入れます。
GitHub にログインした状態で[登録ページ](https://github.com/settings/applications/new)に行き、フォームを埋めていきます。
今回はテスト用のアプリケーションなので、基本的には適当に埋めていけば良いのですが、callback URL だけ以下のように設定します。

***`http://localhost:4000/auth/callback`***

![スクリーンショット 2015-08-31 21.40.48.png](https://qiita-image-store.s3.amazonaws.com/0/77513/a26c32f8-d642-0a08-4433-4887305b838c.png)

Register application をクリックすると、Client ID, Client Secret が表示されます。

# Phoenix アプリケーションをセットアップする

新規でアプリケーションを作成します。

```bash
$ mix phoenix.new github_oauth
```

次に OAuth 認証のライブラリをインストールします。
`mix.exs` に以下の一行を追記します

```ex:mix.exs
...
  def application do
    [mod: {GithubOauth, []},
     applications: [:phoenix, :phoenix_html, :cowboy, :logger,
                    :phoenix_ecto, :postgrex,
                    # 以下を追記
                    :oauth2]]
  end
...
     {:phoenix_html, "~> 2.1"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:cowboy, "~> 1.0"},
     # 以下を追記
     {:oauth2, "~> 0.3"}]
  end
end
```

追記したライブラリをダウンロードしてセットアップ完了です。

```bash
$ mix deps.get
```

# GitHub 認証ストラテジを追加する

`/web/auth/github.ex` というファイルを作成し、以下のように記述します。

```ex:github.ex
defmodule GitHub do
  use OAuth2.Strategy

  def new do
    OAuth2.Client.new([
      strategy: __MODULE__,
      # 生成したクライアントID
      client_id: "XXXXXXXXXXXXXXXXXXXX",
      # 生成したクライアントシークレット
      client_secret: "YYYYYYYYYYYYYYYYYYYY",
      # 設定したコールバックURL
      redirect_uri: "http://localhost:4000/auth/callback",
      # GitHub API のサイト
      site: "https://api.github.com",
      # GitHub が提供する認証用 URL
      authorize_url: "https://github.com/login/oauth/authorize",
      # Github が提供するトークン取得用 URL
      token_url: "https://github.com/login/oauth/access_token"
    ])
  end

  def authorize_url!(params \\ []) do
    new()
    |> put_param(:scope, "user") # スコープはひとまず user のみ
    |> OAuth2.Client.authorize_url!(params)
  end

  def get_token!(params \\ [], headers \\ [], options \\ []) do
    OAuth2.Client.get_token!(new(), params, headers, options)
  end

  def authorize_url(client, params) do
    OAuth2.Strategy.AuthCode.authorize_url(client, params)
  end

  def get_token(client, params, headers) do
    client
    |> put_header("Accept", "application/json")
    |> OAuth2.Strategy.AuthCode.get_token(params, headers)
  end
end
```

`client_id` と `client_secret` は適宜書き換えてください。

# 認証用コントローラを作成する

`/web/controllers/auth_controller.ex` というファイルを作成し、以下のように記述します。

```ex:auth_controller.ex
defmodule GithubOauth.AuthController do
  use GithubOauth.Web, :controller

  plug :action

  def index(conn, _params) do
    # GitHub の認証ページへリダイレクトさせます
    redirect conn, external: GitHub.authorize_url!
  end

  def callback(conn, %{"code" => code}) do
  # 返却されたコードからトークンを取得します
    token = GitHub.get_token!(code: code)

    # アクセストークンを使ってユーザ情報取得 API にリクエストします
    %{body: user} = OAuth2.AccessToken.get!(token, "/user")

    # ユーザ情報をセッションへ詰めた後、ルートページへリダイレクトさせます
    conn
    |> put_session(:current_user, user)
    |> put_session(:access_token, token.access_token)
    |> redirect(to: "/")
  end
end
```

こちらのコントローラでは、先ほど作成した GitHub ストラテジをもとに認証〜ユーザ情報取得を行っています。
基本的な [OAuth 認証の流れ](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2)ですね。

# ルーティングを設定する

今回のアプリケーションのルーティングは以下のようになります。

* */*
    * ログイン済み（ユーザ情報アリ）の場合
        * ユーザ情報を表示する
    * それ以外の場合
        * 認証を促す（/auth へ遷移する）ボタンを表示する
* */auth*
    * GitHub の認証ページをリダイレクトさせます
* */auth/callback*
    * GitHub からのコールバックを処理します
    * 最終的に */* へリダイレクトされます

これを `router.ex` に実装します。

```ex:router.ex
defmodule GithubOauth.Router do
  use GithubOauth.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    # ユーザ情報を読み込む処理をバイプラインに追加します
    plug :assign_current_user
  end

  scope "/", GithubOauth do
    pipe_through :browser
    get "/", PageController, :index
  end

  scope "/auth", GithubOauth do
    pipe_through :browser
    get "/", AuthController, :index
    get "/callback", AuthController, :callback
  end

  # ユーザ情報をセッション情報から読み込みます
  defp assign_current_user(conn, _) do
    assign(conn, :current_user, get_session(conn, :current_user))
  end
end
```

パイプラインに `assign_current_user` を追加している点に注目です。これにより、このパイプラインを through する全てのリクエストで、ログインしているユーザ情報にアクセスできるようになります。

# ページを作る

まず、`web/templates/layout/app.html.eex` から以下の2行を削除します。`fetch_flash` プラグの記載を省いたため flash storage が利用できず、`get_flash/2` でエラーが発生するためです。

```jsp:app.html.eex
...
    <div class="container" role="main">
      <div class="header">
        <!-- ココカラ -->
        <ul class="nav nav-pills pull-right">
          <li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
        </ul>
        <!-- ココマデを削除（なんとなく不恰好なので） -->
        <span class="logo"></span>
      </div>

      <!-- ココカラ -->
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <!-- ココマデを削除 -->

      <%= @inner %>

    </div> <!-- /container -->
...
```

コメントアウトだけだとエラーになるので、しっかりと削除してください。

次に `web/templates/page/index.html.eex` を以下のように編集します。

```jsp:index.html
<div class="row">
  <div class="col-xs-12">
    <%= if @current_user do %>
      <h2>Hello, <%= @current_user["name"] %>!</h2>
      <img src="<%= @current_user["avatar_url"] %>" class="img-circle"/>
    <% else %>
      <a class="btn btn-primary btn-lg" href="<%= auth_path @conn, :index %>">
        Sign in with GitHub
      </a>
    <% end %>
  </div>
</div>
```

ユーザ情報がある場合は名前とアバター画像を、ない場合はログインボタンを表示します。

ちなみに、href に指定している `auth_path @conn, :index` は、ルーティングで設定した auth スコープの :index に相当する URL を取得する関数です。
*auth_path* なんていつ設定したっけ...という感じですが、これは Phoenix が自動で生成してくれているようです。
現在設定されているルーティングの一覧は、以下のコマンドで確認できます。

```bash
$ mix phoenix.routes
page_path  GET  /               GithubOauth.PageController :index
auth_path  GET  /auth           GithubOauth.AuthController :index
auth_path  GET  /auth/callback  GithubOauth.AuthController :callback
```

以上で実装は完了です。

# ログインしてみる

以下のような画面遷移が出来れば成功です。

**i) トップ画面（ログイン後）**
![スクリーンショット 2015-08-31 22.55.14.png](https://qiita-image-store.s3.amazonaws.com/0/77513/602ec6f5-51d2-5e6d-fec1-52a61580c79a.png)

**ii) GitHub 認証画面**
![スクリーンショット 2015-08-31 22.55.28.png](https://qiita-image-store.s3.amazonaws.com/0/77513/a44dce9d-387c-1b92-36d3-b20f2be3c88a.png)

**iii) パスワード入力画面**
![スクリーンショット 2015-08-31 22.56.00.png](https://qiita-image-store.s3.amazonaws.com/0/77513/a30f8cdb-3c0c-4c73-ee5b-1c706e4a02aa.png)

**iv) トップ画面（ログイン後）**
![スクリーンショット 2015-09-01 0.12.29.png](https://qiita-image-store.s3.amazonaws.com/0/77513/9a12b19c-820e-d721-3b86-4103d18d0b7c.png)

# 感想

* OAuth 認証の流れが理解できていればすんなりと実装できる
* パイプライン内で認証済みかどうかを判定してみてもいいかもしれない
    * 今の実装だとログイン後も /auth にアクセスすることでログイン処理が走ってしまう
    * 認証していなかったらログイン画面にリダイレクト、的なほうが好ましそう
* ログアウトする（ユーザ情報をクリアする）処理を組んでもよかったかもしれない

# 追記（2016/01/11）

* oauth2 ライブラリのバージョンが上がり一部仕様が変更されていましたので、内容を修正しました
* @hykw さん、ご指摘いただきありがとうございました :bow: