PlayframeworkでOpenIDを使ったログイン処理

Playには、OpenID 2.0 を使うためのライブラリが用意されてます。
play.api.libs.openid というやつですね。

http://www.playframework-ja.org/documentation/2.0.4/ScalaOpenID

これを使ってOpenID認証してみたので、忘れる前にメモ。
Playのバージョンは、2.1.1です。

OpenID認証してみる

GoogleアカウントでOpenID認証してみることに。
Googleでログインする」みたいなリンクを用意して、そのリンクがクリックされたときのアクションが下のやつです。

object Auth extends Controller {
  def authenticate = Action { implicit request =>
    val f: Future[String] = OpenID.redirectURL(
      "www.google.com/accounts/o8/id",
      routes.Auth.openIDCallback.absoluteURL()
    )
    f onFailure { case _ => Redirect(routes.Application.login) }
    AsyncResult(f map(Redirect(_)))
  }
  def openIDCallback = TODO
}


OpenID.redirectURLメソッドに、OP(OpenID Provider) Identifierと認証が成功した場合のコールバックURLを渡します。コールバックURLはPlayのRouteから取得してます。
OP Identifierは各OPのWebサイトで確認できると思います。今回のOPはGoogle
OpenID.redirectURLメソッドは、OP IdentifierからOPの認証画面のURLを探索してくれます。この探索ではHTTP通信が発生するので、Future[String]を返してきます。
Futureに包まれたStringはOPの認証画面のURLであり、上のauthenticateアクションのレスポンスはそのURLへのリダイレクトとなってます。

リダイレクト先のOPの認証画面で、Googleアカウントの認証が完了すると、第2引数で指定したコールバックURLへリダイレクトされます。

これでOpenIDを使った認証はとりあえずできました。

セッションを使う

次は、ユーザ情報をセッションに格納してみます。
以下のopenIDCallbakがOPの画面での認証後にリダイレクトされるアクション。

object Auth extends Controller {
  def authenticate = Action { implicit request =>
    val f: Future[String] = OpenID.redirectURL(
      "www.google.com/accounts/o8/id",
      routes.Auth.openIDCallback.absoluteURL()
    )
    f onFailure { case _ => Redirect(routes.Application.login) }
    AsyncResult(f map(Redirect(_)))
  }
  def openIDCallback = Action { implicit request =>
    val f: Future[UserInfo] = OpenID.verifiedId
    f onFailure { case _ => Redirect(routes.Application.login) }
    AsyncResult(
      f map( info =>
        Redirect(routes.Application.index).withSession("openid" -> info.id)
      )
    )
  }
}

OpenID.verifiedIdメソッドは、implicitパラメータとしてrequestオブジェクトを受け取ります。
リダイレクトで渡ってきたパラメータが正しいかOPに対して問い合わせるため、結果はFuture[UserInfo]です。

UserInfoには、ユーザを識別するためのIDを持ってます。
こいつをセッションに格納してログイン状態にできます。

ログイン状態をチェックする処理を共通化する

上の例でIDをセッションに格納した状態で、Applicationコントローラのindexアクションへリダイレクトさせました。
indexアクションでは、セッション情報を確認して、ログインしているかどうかで処理を分岐させたい。あと、このログイン判定は共通化したい。

こうゆう共通処理は、アクションの合成ってのを使えばよさそう。
http://www.playframework-ja.org/documentation/2.0.4/ScalaActionsComposition

object Application extends Controller {

  def openid(request: RequestHeader): Option[String] = request.session.get("openid")
  def onUnauthorized(request: RequestHeader): Result = Results.Redirect(routes.Application.login)
  def withAuth(f: => String => Request[AnyContent] => Result) = {
    Security.Authenticated(openid, onUnauthorized) { user =>
      Action(request => f(user)(request))
    }
  }

  def index = withAuth { openid => implicit request =>
    // ・・・
  }
}

まず、withAuthっていうメソッドを用意。このメソッドはカリー化された関数を受け取ってActionを返すようにします。
そして、リダイレクト先となるindexアクションでは、Actionを使う代わりに、withAuthを使ってActionオブジェクトを作ります。

withAuthの実装ですが、ログインしているかどうかで、分岐するようにします。
これをやってくれるヘルパー的なものがPlayには用意されてました。
play.api.mvc.Security.Authenticatedです。
http://www.playframework.com/documentation/api/2.1.1/scala/index.html#play.api.mvc.Security$
第一引数に認証情報を取得してOptionを返す関数を指定します。OptionがNoneだった場合、第二引数に指定する関数が実行され、OptionがSomeだった場合、第三引数に指定する関数が実行されます。
第三引数の関数内で、アクションを作ってます。

こんな感じのwithAuthを用意しておいて、それをトレイトに実装しておくと色んなコントローラで使えそうですね。
Playで、Servletのフィルターみたいなことしたい時はアクションの合成を使う、でいいのかな・・・。

セッション情報を消す

セッション情報を消したい場合は、withNewSessionを使えばいいみたいです。

object Auth extends Controller {
  def delete = Action { implicit request =>          
    Redirect(routes.Application.login).withNewSession
  }
}

おしまい

ドキュメントでアクションの合成って見たときは何のことかと思ったけど、各アクションの共通処理を書けるんですねー。



Scala逆引きレシピ (PROGRAMMER’S RECiPE)

Scala逆引きレシピ (PROGRAMMER’S RECiPE)