Play2の認証ライブラリとしてSecureSocialを使う

PlayFrameworkで認証周りを実装するときSecureSocialというライブラリが便利です。(もともとPlay1.xに対応する認証関連ライブラリとしてPlayの公式サイトにも掲載されてますがPlay2.xにも対応済みで便利に使えます。)

私はNode.jsでexpressを使うときはpassportというライブラリを使うのですが、それと似たような使用感です。

以下、SecureSocialのドキュメントにある通りやったもの。

build.sbt

build.sbtのresolverとlibraryDependenciesに以下のように追加。

resolvers += Resolver.url("sbt-plugin-releases", url("http://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/"))(Resolver.ivyStylePatterns)
libraryDependencies ++= Seq(
  (略)
  "securesocial" %% "securesocial" % "2.1.2" 

このようにリゾルバを追加するくらいだったらBuild.scalaにするまでもなくbuild.sbtで出来るようです。

conf/

conf/にsecuresocial.confファイルを作ってapplication.confから以下のようにincludeするのが定石みたい。

# application.conf
(略)
include "securesocial.conf"

とりあえずTwitter認証と単純なユーザ/パスワード認証の2本立てでいく場合

# securesocial.conf
smtp {
    host=smtp.gmail.com
    #port=25
    ssl=true
    user="xxx@gmail.com"
    password=xxxxx
    from="xxx@gmail.com"
}

securesocial {
    onLoginGoTo=/
    onLogoutGoTo=/login
    ssl=false

    twitter {
    	requestTokenUrl="https://api.twitter.com/oauth/request_token"
    	accessTokenUrl="https://api.twitter.com/oauth/access_token"
    	authorizationUrl="https://api.twitter.com/oauth/authorize"
    	consumerKey=xxxxx
    	consumerSecret=xxxxx
    }
}

として、TwitterDeveloperサイトでconsumerKeyとconsumerSecretを発行してコピペ。(なお、TwitterDeveloperサイトでコールバックURLを指定するのを忘れてはならない。アプリケーションがコールバックURLを指定していることをチェックしたければ指定しないほうがいい的な文言が添えられてるが、指定しないと認証できないので注意。私はこれにハマって丸一日つぶした。)

Facebook認証など他の認証手段も追加したいときはtwitterブロックの下に設定を追加していく。対応してるプロバイダ一覧はこちら

smtpのブロックのところにはユーザ/パスワード認証で使われるユーザ登録確認メールを送るためのメール送信用アカウントを設定する。サービス用のgmailアドレスを取得して指定するのがよいでしょう。

続いて同じくconf/にplay.pluginsというファイルを作って以下を書く。

1500:com.typesafe.plugin.CommonsMailerPlugin
9994:securesocial.core.DefaultAuthenticatorStore
9995:securesocial.core.DefaultIdGenerator
9996:securesocial.core.providers.utils.DefaultPasswordValidator
9997:securesocial.controllers.DefaultTemplatesPlugin
9998:service.MyUserService
9999:securesocial.core.providers.utils.BCryptPasswordHasher
10000:securesocial.core.providers.TwitterProvider
10004:securesocial.core.providers.UsernamePasswordProvider

とりあえずTwitterProviderとUsernamePasswordProviderを使う場合はこのようにし、9998のMyUserServiceのところには以降に書く各認証手段のハンドリング用のクラスを指定する。

続いてconf/にあるroutesファイルに以下を追加。

# Login page
GET     /login                      securesocial.controllers.LoginPage.login
GET     /logout                     securesocial.controllers.LoginPage.logout

ログインページはsecuresocialがbootstrapバリバリのページを用意してくれてるので、必要に応じてそこのデザインを自分のサービスのものに置き換えて使う。コントローラはライブラリが適用してくれるものをそのまま使えばOK。logout後のloginページへのリダイレクトとかちゃんとやってくれる。

認証のハンドリング用クラス

app.serviceというパッケージにMyUserService.scalaのようなコードを置くのが定石みたい。

package service

import play.api.{Logger, Application}

import securesocial.core._
import securesocial.core.providers.Token
import securesocial.core.IdentityId

import models.User;

case class MySocialUser(localUser: User, identityId: IdentityId,
                      firstName: String, lastName: String, fullName: String,
                      email: Option[String],
                      avatarUrl: Option[String], authMethod: AuthenticationMethod,
                      oAuth1Info: Option[OAuth1Info] = None,
                      oAuth2Info: Option[OAuth2Info] = None,
                      passwordInfo: Option[PasswordInfo] = None) extends Identity

object MySocialUser {
  def apply(user: User): MySocialUser = {
    MySocialUser(user, IdentityId(user.name,user.provider), "","","",
      None, None, AuthenticationMethod("dummy"), None, None, None)
  }
}


class MyUserService(application: Application) extends UserServicePlugin(application) {
  private var tokens = Map[String, Token]()

  def find(id: IdentityId): Option[Identity] = {
    val user = User.findByName(id.userId,id.providerId);
    if (user.isEmpty) None else Some(MySocialUser(user.get));
  }

  def findByEmailAndProvider(email: String, providerId: String): Option[Identity] = {
    None // not implemented yet
  }

  def save(i: Identity) = {
    val ii=i.identityId
    var user = User.findByName(ii.userId,ii.providerId);
    if (user.isEmpty) user = User.insert(ii.userId, ii.providerId)
    MySocialUser(user.get)
  }

  def save(token: Token) {
    tokens += (token.uuid -> token)
  }

  def findToken(token: String): Option[Token] = {
    tokens.get(token)
  }

  def deleteToken(uuid: String) {
    tokens -= uuid
  }

  def deleteTokens() {
    tokens = Map()
  }

  def deleteExpiredTokens() {
    tokens = tokens.filter(!_._2.isExpired)
  }
}

Identityトレイトを実装したオブジェクトが、コントローラからrequestオブジェクトのメンバーとして参照可能になる。大抵はユーザ情報をDBで管理すると思うので、上記コードのようにユーザクラス"User"のようなものを作ってIdentityトレイトを継承したクラス(MySocialUser)のメンバーに入れておくのが良いでしょう。

Dao周りの設定が済んでから認証周りを実装したほうがスムーズになりそうです。(私は逆にやったので途中ごちゃっとしたりした)

これでコントローラからは

(略)
import securesocial.core.{Identity, Authorization}

import models._
import service._

object Test extends Controller with securesocial.core.SecureSocial {

  def test = SecuredAction { implicit request =>
    request.user match {
      case u: MySocialUser => Ok(views.html.home(u.localUser.id.toString()));
      case _ => Results.Unauthorized
    }
  }

という風に書ける

まとめ

だいぶハマったのですが、こうブログ記事としてまとめて書きおえてみるとドキュメントに書いてある通りやっただけという感じですが、参考になれば幸いです。