19
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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を実装したクラスを作り、自分のAuthConfigImplidContainerを上書きすることでできます。

AuthConfigImpl
class RDBMSIdContainer ... // 自前のIdContainer

trait AuthConfigImpl extends AuthConfig {

  ...

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

  ...
}

自前のIdContainerを作る

自前の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の雛形を作ります。

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というサービスを作り、認証サーバとやりとりすることを想定したコードにしました。

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ビット以上のトークンなどです。そうしていないと、セッションハイジャックの脆弱性ができてしまいます。

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
    }
  }
}

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

19
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?