buddyでユーザ認証

buddyとは
=========

[buddy][buddy-repo]はClojureのユーザ認証ライブラリです。主に[Andrey Antukh](http://www.niwi.be/)氏により開発が進められているOSSであり、2条項BSDライセンスで配布されています。ClojureでWebアプリ開発を行うときに利用できます。

同種の認証ライブラリとしては[friend][friend-repo]が有名ですが、buddyのほうがシンプルにまとまっている印象を受けます。

以前、[テンクー][xcoo]チームの一員として参加したClojure Cup（Clojure/ClojureScriptの48時間プログラミングコンテスト）で制作したニュースアプリ *[news.async][newsasync]* でも使われています。

インストール
===========

Leiningenを使っているなら、`project.clj`のdependenciesに以下を加えましょう。

```clj:project.clj
[buddy "1.3.0"]
```

上記は、buddyを構成する[buddy-core][buddy-core], [buddy-sign][buddy-sign], [buddy-hashers][buddy-hashers], [buddy-auth][buddy-auth]を全て依存解決してくれます。

```clj:project.clj
[buddy/buddy-core "1.3.0"]
[buddy/buddy-auth "2.0.0"]
```

のように、コンポーネントごとに解決することも可能です。

なお、buddyはJDK7/8のみの対応なのでご注意ください。

使い方
======

buddyにはローレベルなハッシュアルゴリズムなども含まれていますが、本稿では使用機会が多いであろうRing/Compojureでbuddyを利用する方法を紹介します。その他詳細な使用方法は公式の[ドキュメント][buddy-doc]を参照ください。

アクセスルール
-------------

どのリクエストに対して認証を行うか、というルールを記述します。
下記のようなマップを用意すると、`/admin/`で始まるルートに対して認証をかけます。

```clojure
{:pattern #"^/admin/.*"
 :handler admin-access}
```

RingベースのWebアプリの場合、`wrap-access-rules`を使ってルールを適用します。

```clojure
(ns foo.handler
  (:require [buddy.auth :refer [authenticated?]]
            [buddy.auth.accessrules :refer [wrap-access-rules]]))

(def app
  (let [rules [{:pattern #"^/admin/.*" :handler authenticated?}]]
    (-> app-routes
        (wrap-access-rules {:rules rules :policy :allow}))))
```

認証と承認
---------

buddyではAuthentication（認証）とAuthorization（承認）が明確に分けられています。
認証プロセスのバックエンドとしてはHttp Basic, Session, Token, SignedTokenが用意されています。

認証プロセスを適用するには`wrap-authentication`と`wrap-authorization`を使用します。

```clojure
(ns foo.handler
  (:require [buddy.auth.backends.session :refer [session-backend]]
            [buddy.auth.middleware :refer [wrap-authentication wrap-authorization]]))

(def app
  (let [backend (session-backend {:unauthorized-handler unauthorized-handler})]
    (-> app-routes
        (wrap-authentication backend)
        (wrap-authorization backend))))
```

実例
====

実際にCompojureを使ってユーザ認証を行うだけのWebアプリを作ってみましょう。

デモ
----

作ったアプリをHerokuにデプロイしておきました。また、ソースコードはGithubに公開してあります。

- デモ：https://cljac14-buddy.herokuapp.com
- ソース：https://github.com/totakke/cljac14-buddy

![buddy-example-s.png](https://qiita-image-store.s3.amazonaws.com/0/24168/1f801e31-66c5-dc83-e3d8-cf92d037a83d.png)

"To Secure Area" ボタンを押すと、ログインしていなければログインページに飛びます。適当なユーザ名・パスワードを入力して "Log in" ボタンを押すと、`/admin`が見られるようになります。"Logged in as [ユーザ名]" という表示がされているはずです。"Log out" を押すとログアウトします。

レシピ
------

それでは上記デモのWebアプリを順番に作っていきましょう。Herokuで動かすために必要な部分は省略するので、Github上のソースとは異なる部分があります。

まず、Compojureのテンプレートを使って、Leiningenプロジェクトを作成します。

```bash
$ lein new compojure cljac14-buddy
```

`project.clj`を開いて、buddyとページ作成に使用するHiccupを依存関係に追加します。

```clojure:project.clj
:dependencies [[org.clojure/clojure "1.8.0"]
               [compojure "1.5.1"]
               [ring/ring-defaults "0.2.1"]
               [buddy "1.3.0"]   ; <= 追加
               [hiccup "1.0.5"]] ; <= 追加
```

準備ができたので、`handler.clj`に処理を書いていきます。まず必要なライブラリを読み込みます。

```clojure:handler.clj
(ns cljac14_buddy.core.handler
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
            [ring.middleware.anti-forgery :refer [*anti-forgery-token*]] ; <=
            [ring.util.response :refer [redirect]]                       ; <=
            [buddy.auth :refer [authenticated?]]                         ; <=
            [buddy.auth.backends.session :refer [session-backend]]       ; <= 追加
            [buddy.auth.middleware :refer [wrap-authentication           ; <=
                                           wrap-authorization]]          ; <=
            [buddy.auth.accessrules :refer [wrap-access-rules]]          ; <=
            [hiccup.core :refer [html]]))                                ; <=
```

次にルーティングを定義します。`/admin`にアクセスしたとき、認証されていなければログインページにリダイレクトするようにします。その他にログアウトのためのルートも準備しておきましょう。

```clojure:handler.clj
(defroutes app-routes
  (GET "/" [] "Hello, buddy!")
  (GET "/admin" req (admin req))       ; <=
  (GET "/login" req (login-get req))   ; <= 追加
  (POST "/login" req (login-post req)) ; <=
  (GET "/logout" [] (logout))          ; <=
  (route/not-found "Not Found"))
```

`app`にはアクセスルールとして`/admin`を指定します。また、認証プロセスのバックエンドとしてSessionを利用します。すでに述べたように`wrap-access-rules`, `wrap-authentication`, `wrap-authorization`を用いて適用していきます。

```clojure:handler.clj
(def app
  (let [rules [{:pattern #"^/admin$" :handler authenticated?}]
        backend (session-backend {:unauthorized-handler unauthorized-handler})]
    (-> app-routes
        (wrap-access-rules {:rules rules :policy :allow})
        (wrap-authentication backend)
        (wrap-authorization backend)
        (wrap-defaults site-defaults))))
```

`/admin`にアクセスしたとき、承認済みでなければ`unauthorized-handler`が呼ばれます。`unauthorized-handler`では、認証されていなければログインページにリダイレクトするようにします。認証後に元のページに戻ってこられるよう、URLに`next`パラメータを付加しておきます。

```clojure:handler.clj
(defn unauthorized-handler
  [req meta]
  (if (authenticated? req)
    (redirect "/")
    (redirect (format "/login?next=%s" (:uri req)))))
```

各リクエストに対するレスポンスを記述していきます。

adminページではログインユーザ名が表示されるようにしておきましょう。

```clojure:handler.clj
(defn admin
  [req]
  (str "Logged in as " (name (get-in req [:session :identity]))))
```

ログインページにGETリクエストがきたときはログインフォームを表示します。

```clj:handler.clj
(defn login-get
  [req]
  (html
   [:form {:method "post"}
    [:input {:type "text" :name "username"}]
    [:input {:type "password" :name "password"}]
    [:input {:type "hidden" :name "__anti-forgery-token" :value *anti-forgery-token*}]
    [:input {:type "submit"}]]))
```

ここではHiccupを用いてレンダリングしています。`__anti-forgery-token`はCSRFを防ぐ[Ring-Anti-Forgery][ring-anti-forgery]のトークンです。submitボタンが押されると`/login`にPOSTリクエストが送信されます。

![Login page](https://qiita-image-store.s3.amazonaws.com/0/24168/8c2bb53e-229c-41c8-7fec-a89b2569a183.png)

バックエンドにセッションを利用する場合、`request :session :identity`にトークンを保存します。
今回は単純にユーザ名を保存しておきます。

```clj:handler.clj
(defn login-post
  [req]
  (let [username (get-in req [:form-params "username"])]
    (-> (redirect (get-in req [:query-params "next"] "/"))
        (assoc-in [:session :identity] (keyword username)))))
```

これだけで認証は終了です。
元のページ (admin) にリダイレクトされ、自分の名前が表示されていれば成功です。

![Logged in as ...](https://qiita-image-store.s3.amazonaws.com/0/24168/c5e16861-9191-2aaf-9da3-dbb7423f2883.png)


せっかくなので、ログアウトもできるようにします。
ログアウトは簡単で、`request :session`を空にしてしまうだけです。

```clj:handler.clj
(defn logout
  []
  (-> (redirect "/")
      (assoc :session {})))
```

おわりに
========

本稿では、buddyを用いてRing/CompojureベースのWebアプリにユーザ認証を導入する方法を紹介しました。今回は説明のため非常に簡単なWebアプリを作りましたが、実際のWebアプリで利用するにはパスワードをハッシュ化してデータベースに保存したり、セッションの永続化を行ったりといったことが必要になります。

実を言うと直前まで別ネタで書こうとしていたため、少し説明が不十分かもしれませんが、お役に立てば幸いです。


[buddy-repo]: https://github.com/funcool/buddy
[friend-repo]: https://github.com/cemerick/friend
[xcoo]: https://xcoo.co.jp
[clojurecup]: https://clojurecup.com
[newsasync]: http://newsasync.tv
[buddy-core]: https://github.com/funcool/buddy-core
[buddy-auth]: https://github.com/funcool/buddy-auth
[buddy-sign]: https://github.com/funcool/buddy-sign
[buddy-hashers]: https://github.com/funcool/buddy-hashers
[ring-anti-forgery]: https://github.com/ring-clojure/ring-anti-forgery
[buddy-doc]: https://funcool.github.io/buddy/latest/