play2-authはPlay2.xで認証・認可を実装するためのライブラリです。試している最中ですが、シンプルに始められて、後々必要に応じて拡張していけるので気に入っています。ここでは、セッションストレージを自前のものに拡張する方法を説明していきます。
ここ最近play2-authについていくつか記事を書きました。play2-authのサンプルを動かしてみたい場合は、『PlayFramework: play2-authのサンプルを動かしてみたい』が参考になると思います。また、認証・認可の拡張方法については『チュートリアル: play2-authでPlayに独自の認証を実装する』を御覧ください。
play2-authのデフォルトのセッションストレージ
play2-authのデフォルトのAuthConfigでは、セッションはオンメモリになっているようです。少しコードを追ってみましたが、CacheIdContainerというのがデフォルトのストレージ実装であるようで、PlayのCache APIを使っていました。Playのドキュメントによると、Cacheの実装はデフォルトではEhcacheで、その実装は換装可能とのことです。
オンメモリと聞いて、「Playのセッションはたしかstatelessでメモリ上には何も作らないはず?」と思ったかもしれません。play2-authのドキュメントにも書いてありますが、play2-authはPlayの方針とは異なり、stetefulな実装をデフォルトとしています。その理由として日本語ドキュメントに詳しく書いてあります。
このモジュールの標準実装はステートフルな実装になっています。 Play framefork が推奨するステートレスなポリシーを尊重したくはあるのですが、 ステートレスにすると次のようなセキュリティリスクが存在するため、標準では安全側に倒してあります。
例えば、インターネットカフェなどでサービスにログインし、 ログアウトするのを忘れて帰宅してしまった、といった場合。 ステートレスではその事実に気付いても即座にそのSessionを無効にすることができません。 標準実装ではログイン時に、それより以前のSessionを無効にしてます。 したがってこの様な事態に気付いた場合、即座に再ログインすることでSessionを無効化することができます。
play2-authでのセッションストレージ換装方法
Ehanceの設定またはPlayのCacheのデフォルトストレージを変更すれば、オンメモリ以外の選択肢がきそうです。が、今回はCache全体のストレージを変更したいわけではなく、認証セッションのストレージだけを自前のものにしたいので、play2-authの仕組みだけで完結させようと思います。
play2-authでのセッションストレージ換装方法については、play2-authのドキュメントの"Stateless vs Stateful implementation"にも書いてありますが、IdContainerを実装したクラスを作り、自分のAuthConfigImpl
のidContainer
を上書きすることでできます。
class RDBMSIdContainer ... // 自前のIdContainer
trait AuthConfigImpl extends AuthConfig {
...
override lazy val idContainer: AsyncIdContainer[Id] = AsyncIdContainer(new RDBMSIdContainer[Id])
...
}
自前のIdContainer
を作る
自前のIdContainer
を作ろうと思いますが、その前に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
の雛形を作ります。
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
というサービスを作り、認証サーバとやりとりすることを想定したコードにしました。
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ビット以上のトークンなどです。そうしていないと、セッションハイジャックの脆弱性ができてしまいます。
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
}
}
}
ここまでできたら、自前の認証セッションストレージの実装は完了です。