play2-auth: 自前の認証セッションストレージを実装したい

[play2-auth]はPlay2.xで認証・認可を実装するためのライブラリです。試している最中ですが、シンプルに始められて、後々必要に応じて拡張していけるので気に入っています。ここでは、セッションストレージを自前のものに拡張する方法を説明していきます。


ここ最近play2-authについていくつか記事を書きました。play2-authのサンプルを動かしてみたい場合は、『[PlayFramework: play2-authのサンプルを動かしてみたい](http://qiita.com/suin/items/ffa36c01d7d693fb6d3a)』が参考になると思います。また、認証・認可の拡張方法については『[チュートリアル: play2-authでPlayに独自の認証を実装する](http://qiita.com/suin/items/10ce40d0f8478e9a1287)』を御覧ください。

## play2-authのデフォルトのセッションストレージ

play2-authのデフォルトの[AuthConfig]では、セッションはオンメモリになっているようです。少しコードを追ってみましたが、[CacheIdContainer]というのがデフォルトのストレージ実装であるようで、[PlayのCache API]を使っていました。Playのドキュメントによると、Cacheの実装はデフォルトでは[Ehcache]で、その実装は換装可能とのことです。

オンメモリと聞いて、「Playのセッションはたしかstatelessでメモリ上には何も作らないはず？」と思ったかもしれません。play2-authのドキュメントにも書いてありますが、play2-authはPlayの方針とは異なり、stetefulな実装をデフォルトとしています。その理由として[日本語ドキュメント](https://github.com/t2v/play2-auth/blob/master/README.ja.md#stateless)に詳しく書いてあります。

>このモジュールの標準実装はステートフルな実装になっています。 Play framefork が推奨するステートレスなポリシーを尊重したくはあるのですが、 ステートレスにすると次のようなセキュリティリスクが存在するため、標準では安全側に倒してあります。
>
>例えば、インターネットカフェなどでサービスにログインし、 ログアウトするのを忘れて帰宅してしまった、といった場合。 ステートレスではその事実に気付いても即座にそのSessionを無効にすることができません。 標準実装ではログイン時に、それより以前のSessionを無効にしてます。 したがってこの様な事態に気付いた場合、即座に再ログインすることでSessionを無効化することができます。



## play2-authでのセッションストレージ換装方法

Ehanceの設定またはPlayのCacheのデフォルトストレージを変更すれば、オンメモリ以外の選択肢がきそうです。が、今回はCache全体のストレージを変更したいわけではなく、認証セッションのストレージだけを自前のものにしたいので、play2-authの仕組みだけで完結させようと思います。

play2-authでのセッションストレージ換装方法については、[play2-authのドキュメントの"Stateless vs Stateful implementation"](https://github.com/t2v/play2-auth#stateless-vs-stateful-implementation)にも書いてありますが、[IdContainer]を実装したクラスを作り、自分の`AuthConfigImpl`の`idContainer`を上書きすることでできます。

```scala:AuthConfigImpl
class RDBMSIdContainer ... // 自前のIdContainer

trait AuthConfigImpl extends AuthConfig {

  ...

  override lazy val idContainer: AsyncIdContainer[Id] = AsyncIdContainer(new RDBMSIdContainer[Id])

  ...
}
```

## 自前の`IdContainer`を作る

自前の`IdContainer`を作ろうと思いますが、その前に`IdContainer`のインターフェイスを確認しておきます。

```scala:IdContainer
package jp.t2v.lab.play2.auth

trait IdContainer[Id] {
  def startNewSession(userId: Id, timeoutInSeconds: Int): AuthenticityToken
  def remove(token: AuthenticityToken): Unit
  def get(token: AuthenticityToken): Option[Id]
  def prolongTimeout(token: AuthenticityToken, timeoutInSeconds: Int): Unit
}
```

**Method 1: `startNewSession(userId: Id, timeoutInSeconds: Int): AuthenticityToken`**

このメソッドはセッションを開始したときに使われるものです。新しいセッションIDを**ランダムに**生成すること、セッションIDをストレージに保存することを行います。また、既にセッションが作られている場合は、それを**破棄**するようにします。


**Method 2: `def remove(token: AuthenticityToken): Unit`**

トークンをストレージから破棄する処理を実装します。

**Method 3: `def get(token: AuthenticityToken): Option[Id]`**

トークンを元にストレージからユーザのIDを探してくるメソッドです。

**Method 4: `def prolongTimeout(token: AuthenticityToken, timeoutInSeconds: Int): Unit`**

トークンの期限を延長するメソッドです。ストレージ上の期限を`timeoutInSeconds`の値で上書きします。

それでは、自前の`IdContainer`の雛形を作ります。

```scala:app/controllers/ProprietaryIdContainer.scala
package controllers

import jp.t2v.lab.play2.auth.{AuthenticityToken, IdContainer}
import scala.reflect.ClassTag

class ProprietaryIdContainer[Id: ClassTag] extends IdContainer[Id] {
  override def startNewSession(userId: Id, timeoutInSeconds: Int): AuthenticityToken = ???

  override def get(token: AuthenticityToken): Option[Id] = ???

  override def remove(token: AuthenticityToken): Unit = ???

  override def prolongTimeout(token: AuthenticityToken, timeoutInSeconds: Int): Unit = ???
}
```

これをベースに自前のセッションストレージと繋いでみたのが次のコードです。ここでは、`services.AccountService`というサービスを作り、認証サーバとやりとりすることを想定したコードにしました。

```scala:app/controllers/ProprietaryIdContainer.scala
package controllers

import jp.t2v.lab.play2.auth.{AuthenticityToken, IdContainer}
import services.AccountService
import scala.reflect.ClassTag

class ProprietaryIdContainer[Id: ClassTag] extends IdContainer[Id] {
  override def startNewSession(userId: Id, timeoutInSeconds: Int): AuthenticityToken =
    AccountService.session.startSession(userId.toString, timeoutInSeconds)

  override def get(token: AuthenticityToken): Option[Id] =
    AccountService.session.userIdOf(token).map(_.asInstanceOf[Id])

  override def remove(token: AuthenticityToken): Unit =
    AccountService.session.removeToken(token)

  override def prolongTimeout(token: AuthenticityToken, timeoutInSeconds: Int): Unit =
    AccountService.session.changeTokenExpires(token, timeoutInSeconds)
}
```

ちなみに、`AccountService`は下に示したコードになりますが、実装はダミーになっています。ダミーなのでセッションIDは固定です。ある瞬間のデータベースのスナップショットを表現したオブジェクトとして見てください。本来ならちゃんとセキュリティ的に安全な推測されにくいトークンにする必要があります。たとえば、CSPRNGで生成した128ビット以上のトークンなどです。そうしていないと、セッションハイジャックの脆弱性ができてしまいます。

```scala
package services

import models.account.User

// 本当は認証サーバにRESTで問い合わせる
object AccountService {
  private val users = List[User](
    User("6E7D8ACD-C36E-4ACC-87B7-5EC35CBA1423", "admin", models.account.Role.Administrator),
    User("D4EAF402-54CC-469A-AACB-66EF0A0CB0AF", "alice", models.account.Role.Administrator)
  )

  private val tokenUser = List[(String, String)](
    ("6E7D8ACD-C36E-4ACC-87B7-5EC35CBA1423", "470qpgo0yk1jf5x68thgv3x3nes3gqxt"),
    ("D4EAF402-54CC-469A-AACB-66EF0A0CB0AF", "ij9wfyfn3w5zfam5k3helwboyh392sje")
  )

  def userOfId(userId: String): Option[User] = {
    users.find(_.id == userId)
  }

  def authenticate(username: String, password: String): Option[User] = {
    users.find(_.username == username)
  }

  object session {
    def startSession(userId: String, timeoutInSeconds: Int): String = {
      tokenUser.find(_._1 == userId).map(_._2).get
    }

    def userIdOf(token: String): Option[String] = {
      tokenUser.find(_._2 == token).map(_._1)
    }

    def removeToken(token: String): Unit = {
      // do nothing
    }

    def changeTokenExpires(token: String, timeoutInSeconds: Int): Unit = {
      // do noting
    }
  }
}
```

ここまでできたら、自前の認証セッションストレージの実装は完了です。

[play2-auth]: https://github.com/t2v/play2-auth
[CacheIdContainer]: https://github.com/t2v/play2-auth/blob/master/module/src/main/scala/jp/t2v/lab/play2/auth/CacheIdContainer.scala
[AuthConfig]: https://github.com/t2v/play2-auth/blob/master/module/src/main/scala/jp/t2v/lab/play2/auth/AuthConfig.scala
[PlayのCache API]: https://www.playframework.com/documentation/ja/2.3.x/ScalaCache
[Ehcache]: http://ehcache.org/
[IdContainer]: https://github.com/t2v/play2-auth/blob/master/module/src/main/scala/jp/t2v/lab/play2/auth/IdContainer.scala