Elixir/Phoenixにおけるueberauth(認証)とex_admin(管理画面)の連携方法

ex_adminは、Railsのactiveadminと同様なCRUD画面を簡単に作成できる非常に便利なパッケージです。

READMEでは説明されていませんが、ElixirのProtocol機能を使用して、ueberauth等の認証パッケージとの連携が可能ですのでその方法について解説したいと思います。

# 手順

ueberauthによる認証の実装方法から説明するとかなり長くなってしまうため、ueberauth_exampleに対してex_adminを組み込む方法を説明したいと思います。

### 1. ueberauth_exampleの取得
まず、下記からueberauth_exampleを取得してください。

[ueberauth_example](https://github.com/ueberauth/ueberauth_example)

### 2. ex_adminのインストール

次に、上記のueberauth_exampleプロジェクトに対してex_adminをインストールしてください。

[ex_admin](https://github.com/smpallen99/ex_admin)

上記のex_adminのREADMEのうち、config.exsの設定部分まで完了させた上で、`iex -S mix phoenix.server`でアプリケーションを起動し、`http://localhost:4000/admin`にアクセスしてダッシュボードが表示されることを確認してください。

### 3. Plug.ConnモジュールにExAdmin.Authenticationプロトコルを実装

どこの位置でもいい(例えばwebディレクトリの直下等)ので、下記のようなファイルを作成してください。

```ex
# conn.ex
defimpl ExAdmin.Authentication, for: Plug.Conn do
  import Plug.Conn, only: [get_session: 2]
  import UeberauthExample.Router.Helpers

  def use_authentication?(_), do: true

  def current_user(conn) do
    get_session(conn, :current_user)
  end

  def current_user_name(conn) do
    current_user(conn).name
  end

  def session_path(conn, _action) do
    auth_path(conn, :delete)
  end
end
```

このファイルが追加された状態で、ueberauth_exampleアプリケーションにログインし、`http://localhost:4000/admin`にアクセスしてみてください。画面右上に、下記のように「ユーザー名」と「Logout」リンクが追加されていると思います。

![スクリーンショット 2016-02-26 15.46.43.png](https://qiita-image-store.s3.amazonaws.com/0/22932/f255edec-c5bd-92c2-c83e-26f8dce7d128.png)

Logoutリンクをクリックすると、ログアウトされてトップページにリダイレクトされることを確認してください。

### 4. ログイン検証用無名関数の追加

上記までの作業により、管理画面にログインユーザーを表示してログアウトする機能までは作成出来ましたが、ログインしていないユーザーも管理画面にアクセス出来てしまう状態なので、ex_adminの設定に、ログイン検証用の無名関数を追加します。

* 注：この機能はDashboard(register_page関数で作成される静的ページ)だとうまく動作しないので、何か適当なModelを作成して、それに対する管理画面を追加して作業を進めてください。下記では「Hoge」というモデルとそれに対応する管理画面が追加されたという前提で説明いたします。

```ex
# config.exs
config :ex_admin, 
  repo: UeberauthExample.Repo,
  module: UeberauthExample,
  modules: [
    # UeberauthExample.ExAdmin.Dashboard, # Dashboardはコメントアウトしてください。
    UeberauthExample.ExAdmin.Hoge
  ],
  # 下記を追加
  authorize: fn(conn, action, resource_model) -> 
    UeberauthExample.UserAuth.check_logged_in(conn, action, resource_model) 
  end
```

```ex
# user_auth.ex
defmodule UeberauthExample.UserAuth do
  import UeberauthExample.Router.Helpers
  import Plug.Conn, only: [get_session: 2]
  import Phoenix.Controller, only: [redirect: 2]

  def logged_in?(conn) do
    case current_user(conn) do
      nil -> false
      _ -> true
    end
  end

  def current_user(conn) do 
    get_session(conn, :current_user)
  end

  def check_logged_in(conn, _action, _resource_model) do
    if conn.request_path != auth_path(conn, :request) && !logged_in?(conn) do
      conn |> redirect(to: auth_path(conn, :request))
    end
    true
  end
end
```

上記を追加して、ログインしていない状態で`http://localhost:4000/admin/hoges`にアクセスしてみてください。ログイン画面にリダイレクトされると思います。

### 解説

Plug.Connモジュールに対してExAdmin.Authenticationプロトコルを実装した箇所がちょっと分かりにくいと思いますので解説します。

ex_adminには下記のようなファイルが存在しています。

```ex
# lib/ex_admin/authentication.ex
defprotocol ExAdmin.Authentication do
  @fallback_to_any true
  def use_authentication?(conn)
  def current_user(conn)
  def current_user_name(conn)
  def session_path(conn, action)
end

defimpl ExAdmin.Authentication, for: Any do
  def use_authentication?(_), do: false
  def current_user(_), do: nil
  def current_user_name(_), do: nil
  def session_path(_, _), do: ""
end
```

上記がビューでimportされて

```ex
# web/views/layout_view.ex
defmodule ExAdmin.LayoutView do
  ...
  import ExAdmin.Authentication
  ...
end
```

テンプレートファイルで使用されます。

```ex
# templates/layout/admin.html.eex
<%= if use_authentication?(@conn) do %>
  <p class="header-item" id="utility_nav">
    <span class="current_user"><%= current_user_name(@conn) %></span>
  </p>  
<% end %>
```

`Plug.Conn`に対してExAdmin.Authenticationプロトコルを実装していない場合、上記関数の呼び出しは`Any`にフォールバックしますが、`Plug.Conn`に対するプロトコルの実装を追加するとそちらが実行されるようになる、という仕組みのようです。

プロトコルのAnyへのフォールバック機能に関しては下記で説明されています。

[fallback-to-any](http://elixir-lang.org/getting-started/protocols.html#fallback-to-any)

### まとめ

かなり省略して説明してありますので、上記だけだとうまく動作しない箇所があるかもしれませんが、ご容赦頂ければと思いますm(__)m