目次


Play を使った Web アプリケーション開発: 第 1 回

Play Framework と Scala でユーザー認証を管理する

Play、Silhouette、MongoDB によるスターター認証 UI を実装する

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Play を使った Web アプリケーション開発: 第 1 回

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Play を使った Web アプリケーション開発: 第 1 回

このシリーズの続きに乞うご期待。

最近の Web アプリケーションで認証を実装するには、かなりの作業が必要になる可能性があります。ユーザーが複数のメカニズム (資格情報であったり、OAuth1、OAuth2、または OpenID を使用するソーシャル・プロバイダーであったりします) を介して認証を行えるようにする必要があるとともに、ユーザー登録とパスワード・リセットには、一般に e-メール・ベースのフローが必要となり、リクエストとビューの両方がログイン・ユーザーの ID (存在する場合) を認識するようにしなければなりません。

このチュートリアルでは、Play Framework を使用して作成したスターター認証アプリケーションを紹介します。Play は Node.js や Vert.x と同じく、スケーラビリティーを念頭に置いて設計された次世代のリアクティブ Web フレームワークの 1 つです。Play にはネイティブ XML や JSON を扱える機能、開発モードでのインブラウザー・エラー・レポート機能、組み込みのテスト・ヘルパー、Selenium を統合する機能など、開発を容易にするさまざまな機能があります。Play アプリケーションを作成するには Java または Scala を使用することができますが、Scala のほうが推奨されます。リアクティブ・プログラミング・スタイルには関数型言語が最適だからです。Java もバージョン 8 でようやく関数型プログラミングの概念を採用するようになっていますが、Scala に備わっている広範な関数型機能には後れをとっています。

私のスターター・アプリケーションでは、以下の機能を実装することで、実際に Scala と Play をどのように使用しているかを紹介しています。

  • e-メール・ベースのユーザー・サインアップ
  • 資格情報 (e-メールとパスワード)、および OAuth1 による Twitter 認証
  • e-メール・ベースのパスワード・リセット
  • 資格情報と Twitter アカウントの関連付け
  • ユーザー認識ビュー、HTTP リクエスト、Ajax 呼び出しそれぞれの例

このアプリケーションでは、認証処理には Silhouette を、ユーザー情報の永続化には MongoDB を採用しています。あらゆるリクエスト処理と MongoDB の操作は完全に非同期です。皆さんがプロジェクトを作成する際のベースに、このアプリケーションを使用すれば、認証をゼロから実装する手間を省くことができます。

このチュートリアルでは、読者に Scala 言語の基礎知識があることを前提とします (Java のバックグラウンドをお持ちで Scala の基礎知識が必要な方は、developerWorksの連載「多忙な Java 開発者のための Scala ガイド」を読むことをお勧めします)。また最低限、Play を少しでも使った経験があること (コントローラー、ルート、ビューの基礎知識があれば十分です) も前提とします。Play の概要については、Play マニュアルの「Getting started」セクションを参照してください (このチュートリアルのコードでは Play Framework バージョン 2.4.2 を使用しているので、このバージョンのマニュアルを読んでください)。また、この連載の第 2 回を引き続きお読みください。第 2 回では、IBM Bluemix 上に Play アプリケーションをデプロイする方法を紹介します。

環境の構築

このスターター・アプリケーションを作成して実行するには、Play 2.4.2 またはそれ以降のバージョンと MongoDB がお使いのシステムにインストールされている必要があります。

MongoDB と Play をインストールする

MongoDB をインストールするには、MongoDB サイトのダウンロード・セクションに記載されている、お使いのプラットフォームを対象とした説明に従ってください。ほとんどの Linux ディストリビューションでは、対応するパッケージ・マネージャーを使用して MongoDB をインストールすることができます。Mac には Homebrew、Windows には MSI インストーラーを使用することができます。

Play Framework (2.4.0 以降) には Java 8 が必要なので、Java SE 8 SDK がインストールされていることを確認してください。Play をインストールするには、最小限のアクティベーター (起動スクリプトが含まれる 1 MB の ZIP ファイル) をダウンロードして解凍します。起動スクリプトを初めて実行すると、Play のコードと依存関係 (約 450 MB) がダウンロードされます。後で便利なように、アクティベーターのフォルダーをシステム・パスに追加しておくことをお勧めします。

アプリケーションをダウンロードして実行する

サンプル・アプリケーション・プロジェクトは IBM Bluemix DevOps Services でホストされています。このプロジェクトの Git リポジトリーを複製することで、ソース・コードを入手することができます (最初にサインインまたは登録しておく必要があります)。あるいは、このチュートリアルの「ダウンロード」セクションを参照して、このアプリケーションを ZIP ファイルとしてダウンロードすることもできます。コードを入手したら、以下の手順に従ってアプリケーションを実行します。

  1. Twitter Application Management サイトでアプリケーションを登録します。コールバック URL として、「http://dwdemo.com:9000/auth/social/twitter」と入力します。
  2. ホスト・ファイルに「127.0.0.1 dwdemo.com」を追加することで、Twitter に登録したのと同じドメインにアプリケーションの URL が指定されるようにします。
  3. コンシューマー・キーとコンシューマー・シークレットの各プロパティーを設定します。それには、Twitter アプリケーション・ページの対応する値を conf/silhouette.conf ファイルの 28 行目と 29 行目にコピーします。
  4. MongoDB サーバーを起動するために、mongod --dbpath <フォルダー> を実行します。ここで、<フォルダー> は MongoDB がデータベース・ファイルを保管するディレクトリーです。
  5. アプリケーションを起動するために、コードを複製した先のルート・フォルダーにカレント・ディレクトリーを変更して activator run を実行します。

アプリケーションが起動されたら (アプリケーションを初めて起動するときには、しばらく時間がかかります)、http://dwdemo.com:9000 にアクセスしてアプリケーションを開きます。するとウェルカム画面が表示されるので、ユーザーはこの画面で、アプリケーションにサインアップしてアカウントを作成することができます。

アプリケーションの構成

環境の構築が完了しました。これで、アプリケーションの構成を探ることができます。

メインの構成ファイル

メインの構成ファイルは conf/application.conf です。Play が構成プロパティーを探すデフォルトのファイルは、このファイルになります。リスト 1 に、関連するコードを抜粋します。

リスト 1. conf/application.conf
play.modules.enabled += "play.modules.reactivemongo.ReactiveMongoModule"
play.modules.enabled += "module.Module"

mongodb.uri = "mongodb://localhost:27017/demodb"

mail.from="dwplaydemo <mailrobot@dwplaydemo.net>"
mail.reply="No reply <noreply@dwplaydemo.net>"

play.mailer {
  mock = true
  host = localhost
}

play.http.filters = "utils.Filters"
play.http.errorHandler = "utils.ErrorHandler"

include "silhouette.conf"

この application.conf ファイルでは、依存性注入バインディングを構成する 2 つのクラスを宣言しています。ReactiveMongoModule クラスは、リアクティブ Mongo バインディングをアプリケーションのクラスに注入できるようにするクラスで、module.Module クラスは、アプリケーション (主に Silhouette クラス) の注入バインディングを指定するクラスです。mongodb.uri プロパティーは MongoDB への接続方法を定義し、play.mailer はサインアップとリセット・パスワードのフローを開発モードでテストするためのモック・メーリング・サービスをセットアップします。モック・メーラーは e-メールのログをコンソールに出力しますが、本番環境のアプリケーションでは SMTP サーバーを使用する必要があります。

utils.Filters クラスは、アプリケーションのフィルター・パイプラインを定義するクラスであり、現在は POST リクエストをセキュリティー保護するために Play のクロスサイト・リクエスト・フォージェリー (CSRF) フィルターを使用しています。utils.ErrorHandler クラスには、アプリケーションのグローバル・リクエスト・エラー処理ポリシーを設定する役割があります。このクラスは、Internal Server Error あるいは Page Not Found などのエラーが発生した状況でのエラー・ページへのリダイレクトを定義するだけでなく、さらに重要なことに、セキュリティー保護されているリソースへの承認されていないアクセスや、認証されていないアクセスが試行された場合のサインイン・ページへのリダイレクトも定義します。インクルードする silhouette.conf ファイルは、Silhouette 固有の設定を宣言するファイルです。これらの設定については、「Silhouette の概要」セクションで説明します。

アプリケーションのルート

アプリケーション固有のページのルートと、認証フローのルートを定義するのは、conf/routes ファイルです。リスト 2 に、アプリケーション固有のルートを記載します。

リスト 2. アプリケーション固有のルート
# Application
GET     /                           controllers.Application.index
GET     /profile                    controllers.Application.profile

# Rest api
GET     /rest/profile               controllers.RestApi.profile

# Public assets
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)
GET     /webjars/*file              controllers.WebJarAssets.at(file)

このアプリケーションは、2 つのページで構成されています。index ページは、ユーザーを認識するページで、ログイン済みユーザーまたは匿名でアクセスしたユーザーを扱います。プロファイル・ページは、セキュリティーで保護されており、承認を受けたアクセスが要求されます (承認を受けていないアクセスは、index ページにリダイレクトされます)。rest/profile URL は、ログイン済みユーザーのプロファイル情報を JSON フォーマットで返す、セキュアな REST API エンドポイントにマッピングされます。最後の 2 つの URL は、スタイルシート、画像、JavaScript コードなどの公開アセットの標準的なルートです。リスト 3 に、認証のルートとフローを記載します。

リスト 3. 認証ルート
GET     /auth/signup                controllers.Auth.startSignUp
POST    /auth/signup                controllers.Auth.handleStartSignUp
GET     /auth/signup/:token         controllers.Auth.signUp(token:String)

GET     /auth/reset                 controllers.Auth.startResetPassword
POST    /auth/reset                 controllers.Auth.handleStartResetPassword
GET     /auth/reset/:token          controllers.Auth.resetPassword(token:String)
POST    /auth/reset/:token          controllers.Auth.handleResetPassword(token:String)

GET     /auth/signin                controllers.Auth.signIn
POST    /auth/authenticate          controllers.Auth.authenticate
GET     /auth/social/:providerId    controllers.Auth.socialAuthenticate(providerId:String)
GET     /auth/signout               controllers.Auth.signOut

サインアップ・フローは、サインアップ・ページに対する GET リクエストによって開始されます。このページから、ユーザーはサインアップ情報 (e-メール・アドレス、パスワード、ファーストネーム、ラストネーム) を /auth/signupPOST 送信します。アプリケーションは、この POST リクエストに対する処理として、/auth/signup/:token URL を記載した e-メール・メッセージをユーザーに送信します。ユーザーがサインアップ・プロセスを完了するには、この URL にアクセスしなければなりません。パスワード・リセットのフローも同様です。ユーザーがパスワード・リセット・ページに対して GET リクエストを送信し、そのページで e-メール・アドレスを /auth/resetPOST 送信します。この POST リクエストにより、/auth/reset/:token URL を記載した e-メール・メッセージが生成されます。ユーザーがこの URL にアクセスすると表示されるページでは、ユーザーが入力した新規パスワードを /auth/reset/:tokenPOST 送信して、パスワード・リセット・プロセスを完了します。

/auth/signin は、サインイン・ページにアクセスするためのルートです。auth/authenticate ルートは資格情報を認証するための URL エンドポイントで、/auth/social/:providerId はソーシャル認証を行うための URL エンドポイントです。現在、サポートされているソーシャル・プロバイダーは、OAuth1 を使用する Twitter だけとなっています。/auth/signout ルートは、アプリケーションからサインアウトするためのエンドポイントを公開します。

ユーザーと身元識別プロファイルのモデル化

このアプリケーションでは、さまざまな認証プロバイダーから提供される複数の身元識別プロファイル (このアプリケーションの場合、資格情報または Twitter OAuth1) とユーザーを関連付けられるように、アカウントの関連付けを実装しています。身元識別プロファイルは Profile クラスによって表します。身元識別プロファイルのリストは User クラスに保持されます。該当するコードを app/models/User.scala から抜粋してリスト 4 に示します。

リスト 4. ユーザー・モデル
case class Profile(
  loginInfo:LoginInfo,
  confirmed: Boolean,
  email:Option[String],
  firstName: Option[String], 
  lastName: Option[String],
  fullName: Option[String], 
  passwordInfo:Option[PasswordInfo], 
  oauth1Info: Option[OAuth1Info],
  avatarUrl: Option[String])

case class User(id: UUID, profiles: List[Profile]) extends Identity {
  def profileFor(loginInfo:LoginInfo) = profiles.find(_.loginInfo == loginInfo)
  def fullName(loginInfo:LoginInfo) = profileFor(loginInfo).flatMap(_.fullName)
}

object User {
  implicit val passwordInfoJsonFormat = Json.format[PasswordInfo]
  implicit val oauth1InfoJsonFormat = Json.format[OAuth1Info]
  implicit val profileJsonFormat = Json.format[Profile]
  implicit val userJsonFormat = Json.format[User]
}

身元識別プロファイル (従って、ユーザー) は、Silhouette の LoginInfo クラスによって一意に識別されます。これは基本的に、(ユーザー ID、プロバイダー ID) タプルです。プロファイルは確認済みになっているか、確認を待っているかのいずれかの状態になります。このフィーチャーは、資格情報プロバイダーが関連付けられているプロファイルでは重宝します。これらのプロファイルは、サインアップ・プロセスの最終ステップとして確認が必要となるためです。プロファイルには基本的な身元識別情報 (e-メール・アドレス、ユーザー名、アバター URL) も含められますが、身元識別情報はプロバイダーによって異なるため、これらの情報はすべてオプションです。資格情報プロバイダーが関連付けられているプロファイルには、ハッシュ化されたパスワードを保持する、Silhouette の PasswordInfo オブジェクトが格納されます。OAuth1 Twitter プロバイダーによって作成されるプロファイルには、Silhouette の OAuth1Info インスタンスが格納されます。このインスタンスに含まれるのは、認証トークンとシークレット・データです。その他の認証プロバイダーをサポートするには、Profile クラスを継承して、さらにフィールド (例えば、OAuth2 の場合は oauth2Info:OAuth2Info プロパティー) を追加する必要があります。

プロファイルのリストのラッパーである User クラスは、2 つのコンビニエンス・アクセサーを提供します。これらのアクセサーは、特定の LoginInfo に関連付けられたプロファイルとフルネームのそれぞれに使用されます。User コンパニオン・オブジェクトは、モデル・クラスと JSON との間の自動変換を宣言します。MongoDB ドライバーが扱うのは JSON オブジェクトであるため、この変換が必要になります。

モデルの永続化

このアプリケーションでは、User クラス、PasswordInfo クラス、OAuth1Info クラスのそれぞれを対象としたデータ・アクセス・オブジェクト (DAO) 内に永続化コードをカプセル化しています。app/daos/UserDao.scala を調べると、UserDao トレイトが見つかります (リスト 5 を参照)。

リスト 5. UserDao トレイト
trait UserDao {
  def save(user:User):Future[User]
  def find(loginInfo:LoginInfo):Future[Option[User]]
  def find(userId:UUID):Future[Option[User]]
  def confirm(loginInfo:LoginInfo):Future[User]
  def link(user:User, profile:Profile):Future[User]
  def update(profile:Profile):Future[User]
}

ユーザー情報を永続化する際や、照会する際には、ID または LoginInfo を使用することができます。DAO には、身元識別プロファイルを確認する操作や、新規の身元識別プロファイルをユーザーに関連付ける操作、さらには身元識別プロファイルを更新する操作も実装されています。この DAO に備わっている非同期の特性として、すべての操作は Future のインスタンスを返すことに注意してください。Future は、非同期で実行される計算処理をモデル化するための Scala の標準的なクラスです。app/daos/UserDao.scala には、UserDao トレイトの MongoDB 実装も含まれています (リスト 6 を参照)。

リスト 6. MongoUserDao クラス
class MongoUserDao extends UserDao {
  lazy val reactiveMongoApi = current.injector.instanceOf[ReactiveMongoApi]
  val users = reactiveMongoApi.db.collection[JSONCollection]("users")

  def find(loginInfo:LoginInfo):Future[Option[User]] = 
    users.find(Json.obj("profiles.loginInfo" -> loginInfo)).one[User]

  def find(userId:UUID):Future[Option[User]] =
    users.find(Json.obj("id" -> userId)).one[User]
  
  def save(user:User):Future[User] =
    users.insert(user).map(_ => user)

  def confirm(loginInfo:LoginInfo):Future[User] = for {
    _ <- users.update(Json.obj(
      "profiles.loginInfo" -> loginInfo
    ), Json.obj("$set" -> Json.obj("profiles.$.confirmed" -> true)))
    user <- find(loginInfo)
  } yield user.get

  def link(user:User, profile:Profile) = for {
    _ <- users.update(Json.obj(
      "id" -> user.id
    ), Json.obj("$push" -> Json.obj("profiles" -> profile)))
    user <- find(user.id)
  } yield user.get

  def update(profile:Profile) = for {
    _ <- users.update(Json.obj(
      "profiles.loginInfo" -> profile.loginInfo
    ), Json.obj("$set" -> Json.obj("profiles.$" -> profile)))
    user <- find(profile.loginInfo)
  } yield user.get
}

MongoUserDao クラスは、Play の依存性注入機能によってリアクティブ Mongo API へのフックを取得するとともに、ユーザー情報を格納するコレクションへの参照を取得します。その後は、MongoDB のコレクション API を使用して Play の JSON オブジェクトを扱います。Silhouette には PasswordInfo クラスの DAO と OAuth1Info クラスの DAO も必要です。これらの実装は、MongoUserDao クラスと同様です。この 2 つの DAO は、完全なソース・コードの app/daos/PasswordInfoDao.scala と app/daos/OAuth1InfoDao.scala に見つかります。

DAO をテストする

永続化コードは認証メカニズムの土台であるため、先に進む前に、このコードが正しく動作することを確認しておくのが賢明です。Play には、テストの作成を単純化するヘルパーとスタブが提供されています。永続化コードをテストするために、ここでは Play の FakeApplication クラスを使用します。このクラスが実行される際には、実際のアプリケーションと同じ構成が使用されますが、唯一の例外として、mongodb.uri プロパティーはテスト・データベースを指しています。リスト 7 に、test/daos/DaoSpecResources.scala から抜粋したコードを記載します。

リスト 7. テスト用フェイク・アプリケーションを作成する
def fakeApp = FakeApplication(additionalConfiguration = 
     Map("mongodb.uri" -> "mongodb://localhost:27017/test"))

def withUserDao[T](t:MongoUserDao => T):T = running(fakeApp) {
  val userDao = new MongoUserDao
  Await.ready(userDao.users.drop(), timeout)
  t(userDao)
}

上記のコードでは、フェイク・アプリケーションを宣言した後、汎用の withUserDao メソッドを定義しています。このメソッドで受け取る関数が、MongoUserDao を引数に取って実際のテストを行います。この実際のテストを行う関数は、フェイク・アプリケーションのテスト・データベースにある users コレクションが消去された後に、フェイク・アプリケーションのコンテキストで実行されます。withUserDao メソッドを使用することで、一連の specs2 テストを実行することができます。リスト 8 に specs2 テストの一例として、test/daos/UserSpecDao.scala に含まれるテストを記載します。

リスト 8. specs2 のユーザー DAO テストの例
"UserDao" should {
  "save users and find them by userId" in withUserDao { userDao =>
    val future = for {
      _ <- userDao.save(credentialsTestUser)
      maybeUser <- userDao.find(credentialsTestUser.id)
    } yield maybeUser.map(_ == credentialsTestUser)
    Await.result(future, timeout) must beSome(true)
  }
}

ユーザー・サービス

Silhouette には、認証作業を行うための IdentityService トレイトの実装が必要です。リスト 9 に、その実装 (注入された UserDao のラッパー) を記載します。この実装は、app/services/UserService.scala に含まれています。

リスト 9. ユーザー・サービス・クラス
class UserService @Inject() (userDao:UserDao) extends IdentityService[User] {
  def retrieve(loginInfo:LoginInfo) = userDao.find(loginInfo)
  def save(user:User) = userDao.save(user)
  def find(id:UUID) = userDao.find(id)
  def confirm(loginInfo:LoginInfo) = userDao.confirm(loginInfo)
  def link(user:User, socialProfile:CommonSocialProfile) = {
    val profile = toProfile(socialProfile)
    if (user.profiles.exists(_.loginInfo == profile.loginInfo)) 
      Future.successful(user) else userDao.link(user, profile)
  }

  def save(socialProfile:CommonSocialProfile) = {
    val profile = toProfile(socialProfile)
    userDao.find(profile.loginInfo).flatMap {
      case None => userDao.save(User(UUID.randomUUID(), List(profile)))
      case Some(user) => userDao.update(profile)
    }
  }

  private def toProfile(p:CommonSocialProfile) = Profile(
    loginInfo = p.loginInfo,
    confirmed = true,
    email = p.email,
    firstName = p.firstName,
    lastName = p.lastName,
    fullName = p.fullName,
    passwordInfo = None,
    oauth1Info = None,
    avatarUrl = p.avatarURL
  )
}

サインアップ・フローが実行されている間にユーザー情報を永続化するのは save(user:User) メソッドです。ユーザーがソーシャル・プロバイダーによって認証済みの場合は、save(p:CommonSocialProfile) メソッドによって処理されます。この場合、指定されたプロファイルを持つユーザーが存在しなければ、アプリケーションによって新規ユーザーが作成され、存在する場合は対応する身元識別プロファイルが更新されます。

ユーザー・トークン

このアプリケーションは、サインアップ・フローの中と、パスワード・リセット・フローの中で、ユーザー・トークンを生成します。ユーザー・トークンは e-メールでユーザーに送信されます。ユーザーは、メールで送信されたトークン ID に基づく URL にアクセスして、フローを続行しなければなりません。models/UserToken.scala ファイルは、ユーザー情報、トークン ID、有効期限データを保持するクラスとして、トークンを実装しています (リスト 10 を参照)。

リスト 10. ユーザー・トークン
case class UserToken(id:UUID, userId:UUID, email:String, expirationTime:DateTime, isSignUp:Boolean) {
  def isExpired = expirationTime.isBeforeNow
}

object UserToken {
  implicit val toJson = Json.format[UserToken]
  
  def create(userId:UUID, email:String, isSignUp:Boolean) = 
    UserToken(UUID.randomUUID(), userId, email, new DateTime().plusHours(12), isSignUp)
}

ユーザー・トークンは MongoDB コレクションに永続化されるため、必要となる JSON フォーマット設定をコンパニオン・オブジェクトが定義しています。その後の処理は、ユーザー情報を処理するときとまったく同じです。アプリケーションは、UserTokenService クラス (services/UserTokenService.scala 内) を使用してトークンを操作します。このサービス・クラスが、注入されたユーザー・トークン DAO をラップします (リスト 11 を参照)。

リスト 11. ユーザー・トークン・サービス
class UserTokenService @Inject() (userTokenDao:UserTokenDao) {
  def find(id:UUID) = userTokenDao.find(id)
  def save(token:UserToken) = userTokenDao.save(token)
  def remove(id:UUID) = userTokenDao.remove(id)
}

UserTokenDao は、MongoUserTokenDao によって実装されるトレイトです。UserTokenDao コードの内容は、ユーザー DAO と同様です。このコードは daos/UserTokenDao.scala にあります。

Silhouette の概要

Silhouette フレームワークの主な特徴は、その柔軟性にあります。Silhouette は一連のスタンドアロンの認証コンポーネントを実装しているため、これらのコンポーネントを構成して組み合わせることで、認証ロジックを組み立てるのは開発者の仕事です。主要なコンポーネントには、以下のものがあります。

  • 身元識別サービス: Silhouette では、ユーザー情報の読み取りに関連するすべての操作の処理を IdentityService トレイトの実装に依存しています。そのため、ユーザー管理はこのフレームワークから完全に切り離されます。「ユーザー・サービス」セクションで説明した UserService クラスが、MongoDB によってサポートされた身元識別サービスを実装します。
  • AuthInfoRepository: Silhouette はユーザー資格情報を永続化する方法を把握する必要があるため、このジョブを AuthInfoRepository トレイトの実装に委任します。このアプリケーションは、「モデルの永続化」セクションで説明した PasswordInfoDao クラスと OAuth1InfoDao クラスを結合した複合リポジトリーを使用します。
  • 認証子: 認証子は、認証成功後のユーザーを追跡します。これらの認証子は、自身の有効性ステータスやユーザーのログイン情報などのデータを格納するトークンです。Silhouette には、クッキー、Play のステートレス・セッション、HTTP ヘッダー、および JSON Web トークン (JWT) をベースとした実装があります。
  • 認証子サービス: すべての認証子には、それぞれに関連付けられた認証子サービスがあり、認証子のライフサイクル (作成、初期化、更新、再作成、失効) に関する処理は、この認証子サービスが行います。
  • 環境: 環境は、Silhouette アプリケーションが必要とする主要なコンポーネントを定義します。環境のタイプは、ユーザーおよび認証子のタイプ (このアプリケーションでは、リスト 4 で定義された User クラスおよび CookieAuthenticator) によってパラメーター化されます。環境を構築するには、身元識別サービスの実装 (UserService) と認証子サービスの実装を渡します。このアプリケーションでは、CookieAuthenticator タイプに必要な CookieAuthenticatorService クラスを使用しています。
  • プロバイダー: プロバイダーとは、ユーザーの認証を扱うサービスのことです。このアプリケーションでは、Silhouette の CredentialsProvider (ローカル認証の場合) と OAuth1 の TwitterProvider を使用します。
  • SocialProviderRegistry: これは、このアプリケーションでサポートしているすべてのソーシャル・プロバイダーのプレースホルダーです。この例の場合、ここには TwitterProvider インスタンスが含まれます。

Silhouette のコンポーネントを構成する

Silhouette のコンポーネントを構成および結合する手段としては、依存性注入が用いられます。Play では Google Guice をデフォルトの依存性注入実装として使用しています (必要に応じて、他の実装を組み込むこともできます)。Guice の module.Module クラスは、リスト 12 に示す基本宣言を先頭に、Silhouette に必要なバインディングを定義します。

リスト 12. 依存性注入バインディング
class Module extends AbstractModule with ScalaModule {

  def configure() {
    bind[IdentityService[User]].to[UserService]
    bind[UserDao].to[MongoUserDao]
    bind[UserTokenDao].to[MongoUserTokenDao]
    bind[DelegableAuthInfoDAO[PasswordInfo]].to[PasswordInfoDao]
    bind[DelegableAuthInfoDAO[OAuth1Info]].to[OAuth1InfoDao]
    bind[IDGenerator].toInstance(new SecureRandomIDGenerator())
    bind[PasswordHasher].toInstance(new BCryptPasswordHasher)
    bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false))
    bind[EventBus].toInstance(EventBus())
    bind[Clock].toInstance(Clock())
  }
  
  // ... Bindings for Silhouette components follow
}

リスト 12 のコードは、Silhouette の身元識別サービスのバインディングを定義しています。具体的には、ユーザー情報、パスワード、および OAuth1 の DAO のバインティングです。また、Silhouette の主要なコンポーネントに必要なオブジェクトもいくつか定義しています。リスト 13 に、同じく module.Module クラスでのこれらのコンポーネントの定義を示します。

リスト 13. 依存性注入バインディング (続き)
@Provides def provideEnvironment(
    identityService: IdentityService[User],
    authenticatorService: AuthenticatorService[CookieAuthenticator],
    eventBus: EventBus): Environment[User, CookieAuthenticator] = {
  Environment[User, CookieAuthenticator](identityService, authenticatorService, Seq(), eventBus)
}

@Provides def provideAuthenticatorService(
    fingerprintGenerator: FingerprintGenerator,
    idGenerator: IDGenerator,
    configuration: Configuration,
    clock: Clock): AuthenticatorService[CookieAuthenticator] = {
  val config = configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator")
  new CookieAuthenticatorService(config, None, fingerprintGenerator, idGenerator, clock)
}

@Provides def provideCredentialsProvider(
    authInfoRepository: AuthInfoRepository,
    passwordHasher: PasswordHasher): CredentialsProvider = {
  new CredentialsProvider(authInfoRepository, passwordHasher, Seq(passwordHasher))
}

@Provides def provideAuthInfoRepository(
  passwordInfoDAO: DelegableAuthInfoDAO[PasswordInfo],
  oauth1InfoDAO: DelegableAuthInfoDAO[OAuth1Info]): AuthInfoRepository = {
   new DelegableAuthInfoRepository(passwordInfoDAO, oauth1InfoDAO)
}

@Provides def provideTwitterProvider(
    httpLayer: HTTPLayer,
    tokenSecretProvider: OAuth1TokenSecretProvider,
    configuration: Configuration): TwitterProvider = {
  val settings = configuration.underlying.as[OAuth1Settings]("silhouette.twitter")
  new TwitterProvider(httpLayer, new PlayOAuth1Service(settings), tokenSecretProvider, settings)
}

@Provides def provideOAuth1TokenSecretProvider(
    configuration: Configuration, clock: Clock): OAuth1TokenSecretProvider = {
  val cfg = configuration.underlying.as[CookieSecretSettings]("silhouette.oauth1TokenSecretProvider")
  new CookieSecretProvider(cfg, clock)
}

@Provides def provideSocialProviderRegistry(
    twitterProvider: TwitterProvider): SocialProviderRegistry = {
  SocialProviderRegistry(Seq(twitterProvider))
}

CookieAuthenticator を使用して User インスタンスを認証する Environment は、以下のものによってインスタンス化されます。

  • UserService クラスにバインドされた IdentityService
  • CookieAuthenticatorService にバインドされた AuthenticatorService
  • リクエスト・プロバイダーの空のリスト (このアプリケーションではリクエスト・プロバイダーを使用しません)
  • 認証イベントをブロードキャストするために使用できる EventBus (このアプリケーションでは使用しません)

CredentialsProvider を作成する際には、資格情報の永続化を PasswordInfoDao クラスと Oauth1InfoDao クラスに委任する DelegableAuthInfoRepository 実装から返される AuthInfoRepository が注入されます。TwitterProvider クラスには OAuth1TokenSecretProvider トレイトの実装が必要です。このトレイトは、OAuth1 の実行中にトークン・シークレットを永続化する方法を定義します。最後にアプリケーションは、使用可能な唯一のソーシャル・プロバイダーとして TwitterProvider を指定する SocialProviderRegistry を定義します。

Silhouette の構成ファイル

クッキー認証子サービス、Twitter プロバイダー、OAuth1 トークン・シークレット・プロバイダーの各バインディングは、conf/silhouette.conf ファイルで定義されている構成プロパティーにアクセスします (リスト 14 を参照)。このファイルが、メインの conf/application.conf ファイルによってインクルードされます。

リスト 14. Silhouette の構成ファイル
authenticator.cookieName="authenticator"
authenticator.cookiePath="/"
authenticator.secureCookie=false
authenticator.httpOnlyCookie=true
authenticator.useFingerprinting=true
authenticator.authenticatorIdleTimeout=30 minutes
authenticator.authenticatorExpiry=12 hours

oauth1TokenSecretProvider.cookieName="OAuth1TokenSecret"
oauth1TokenSecretProvider.cookiePath="/"
oauth1TokenSecretProvider.secureCookie=false
oauth1TokenSecretProvider.httpOnlyCookie=true
oauth1TokenSecretProvider.expirationTime=5 minutes

twitter.requestTokenURL="https://api.twitter.com/oauth/request_token"
twitter.accessTokenURL="https://api.twitter.com/oauth/access_token"
twitter.authorizationURL="https://api.twitter.com/oauth/authorize"
twitter.callbackURL="http://dwdemo.com:9000/auth/social/twitter"
twitter.consumerKey=${?TWITTER_CONSUMER_KEY}
twitter.consumerSecret=${?TWITTER_CONSUMER_SECRET}

上記に示されているプロパティーの詳細な説明については、Silhouette のマニュアルを参照してください。注意する点として、環境変数の TWITTER_CONSUMER_KEYTWITTER_CONSUMER_SECRET を使用することで、ソース・コード内に OAuth1 シークレットが公開されないようにしています。

セキュア・アクション

Silhouette は、セキュリティー保護されたリクエストのハンドラーを実装するのに役立つ以下の 2 つのアドホック・アクションを定義しています。これらのアクションは、Silhouette コントローラー・トレイトの中に混在する複数のコントローラーで使用することができます。

  • UserAwareAction: 認証済みユーザーは、このアクションを実行することができます。その場合、このアクションによって受信されるリクエストには、認証済みユーザーから送信されたリクエストに定義される Option[U] タイプ (アプリケーションに依存するユーザー・タイプ U) の identity プロパティー (身元識別プロパティー) が含まれるようになります。
  • SecuredAction: このアクションを実行するのは、認証済みユーザーでなければなりません。認証されていないユーザーが実行すると、アプリケーションのエラー・ハンドラーの onNotAuthorized メソッドが呼び出されます (このアプリケーションの場合は、「アプリケーションの構成」セクションで説明したようにサインイン・ページにリダイレクトされます)。このアクションは、リクエストの identity プロパティー (身元識別プロパティー) にタイプ U (アプリケーションに依存するユーザー・タイプ U) を設定します (リスト 15 を参照)。
    リスト 15. Silhouette でのセキュアなリクエスト
    class ApplicationController extends Silhouette {
      def someUserAwareAction = UserAwareAction.async {implicit request => 
        request.identity match {
          case None => // Request sent anonymously ...
          case Some(u) => // Request sent by authenticated user u
        }
      }
      
      def someSecureAction = SecureAction.async { implicit request =>
        logger.info(s"Logged user: ${request.identity}")
        ...
      }
    }

ユーザーの管理と認証

ここまでで、ユーザー・モデルと Silhouette の構成をひと通り説明したので、認証コードについて理解するお膳立ては整いました。これから、サインアップと認証を詳細に検討します。このセクションに記載するすべてのコード・スニペットは、controllers/Auth.scala ファイルの Auth コントローラーから抜粋したものです。Auth コントローラーは、「Silhouette の概要」セクションで説明したすべての Silhouette コンポーネントとインターフェースをとるだけでなく、ユーザーおよびユーザー・トークン・サービスとも連動します。これらのコンポーネントのすべてをコントローラーのコンストラクターに注入する必要があります。このコントローラーはセキュアなリクエスト・ハンドラーを実装しており、Silhouette コントローラー・トレイトの中に混在しています (「セキュア・アクション」セクションを参照)。リスト 16 に、Auth コードを記載します。

リスト 16. Auth コントローラー・クラスの宣言
class Auth @Inject() (
  val messagesApi: MessagesApi, 
  val env:Environment[User,CookieAuthenticator],
  socialProviderRegistry: SocialProviderRegistry,
  authInfoRepository: AuthInfoRepository,
  credentialsProvider: CredentialsProvider,
  userService: UserService,
  userTokenService: UserTokenService,
  avatarService: AvatarService,
  passwordHasher: PasswordHasher,
  configuration: Configuration,
  mailer: Mailer) extends Silhouette[User,CookieAuthenticator] {
  
    // ... auth controller code ...
}

ユーザーのサインアップ

サインアップのフローは、startSignUp メソッドで開始します。リスト 17 に記載するように、startSignUp はユーザーを認識する非同期リクエスト・ハンドラーです。

リスト 17. startSignUp メソッド
def startSignUp = UserAwareAction.async { implicit request =>
  Future.successful(request.identity match {
    case Some(user) => Redirect(routes.Application.index)
    case None => Ok(views.html.auth.startSignUp(signUpForm))
  })
}

ユーザーが当該のリクエストと関連付けられている場合、このメソッドはユーザーを index ページにリダイレクトします。そうでない場合は、図 1 に示すサインアップ・ページを表示します。

図 1. サインアップ・ページ
ユーザーの e-メール・アドレス、ファーストネーム、ラストネーム、パスワードの入力を求めるサインアップ・ページのスクリーンショット
ユーザーの e-メール・アドレス、ファーストネーム、ラストネーム、パスワードの入力を求めるサインアップ・ページのスクリーンショット

サインアップ・ページは、ユーザーの e-メール・アドレス、ファーストネーム、ラストネーム、そしてパスワード (確認のために 2 回) の入力を求めるフォームで構成されています。送信されたフォームは、handleStartSignUp メソッドによって処理されます (リスト 18 を参照)。

リスト 18. handleStartSignUp メソッド
def handleStartSignUp = Action.async { implicit request =>
  signUpForm.bindFromRequest.fold(
    bogusForm => Future.successful(BadRequest(views.html.auth.startSignUp(bogusForm))),
    signUpData => {
      val loginInfo = LoginInfo(CredentialsProvider.ID, signUpData.email)
      userService.retrieve(loginInfo).flatMap {
        case Some(_) => 
          Future.successful(Redirect(routes.Auth.startSignUp()).flashing(
            "error" -> Messages("error.userExists", signUpData.email)))
        case None => 
          val profile = Profile(
            loginInfo = loginInfo, confirmed=false, email=Some(signUpData.email), 
            firstName=Some(signUpData.firstName), lastName=Some(signUpData.lastName), 
            fullName=Some(s"${signUpData.firstName} ${signUpData.lastName}"),
            passwordInfo = None, oauth1Info = None, avatarUrl = None)
          for {
            avatarUrl <- avatarService.retrieveURL(signUpData.email)
            user <- userService.save(User(id = UUID.randomUUID(),
              profiles = List(profile.copy(avatarUrl = avatarUrl))))
            _ <- authInfoRepository.add(loginInfo, passwordHasher.hash(signUpData.password))
            token <- userTokenService.save(UserToken.create(user.id, signUpData.email, true))
          } yield {
            mailer.welcome(profile, link = routes.Auth.signUp(token.id.toString).absoluteURL())
            Ok(views.html.auth.finishSignUp(profile))
          }
      }
    }
  )
}

リスト 18 のコードは、リクエスト・フォームを signUpForm クラスにバインドするところから始まります。フォームが無効であるために (e-メール・アドレスが有効でない、ファーストネームまたはラストネームのフィールドが空、パスワードが一致しないなど) バインディングに失敗すると、このメソッドは再びサインアップ・ページを表示して検証エラーがあったことを示します。フォームが有効な場合、このメソッドは受け取った e-メール・アドレスで登録されたユーザーがシステムに存在するかどうかを最初にチェックします。存在する場合、ユーザーはエラー・メッセージを表示するサインアップ・ページにリダイレクトされます。

ユーザーがすべてのチェックに合格すると、このメソッドはフォームのサインアップ・データを使って身元識別プロファイルをインスタンス化し、そのプロファイルを含むユーザー情報を永続化するために userService.save を呼び出します。続いて authInfoRepository.add (PasswordInfoDao.save に委任するメソッド) を呼び出して資格情報を永続化し、トークンを作成します。サインアップ・プロセスは、トークン ID を記載したウェルカム e-メールを送信し、受信される e-メールをチェックするよう指示するサインアップ完了ページにユーザーをリダイレクトして、プロセスを完了します。ウェルカム e-メールには /auth/signup/:token ルートへのリンクが記載されています。このルートは signUp メソッド (リスト 19 を参照) にマッピングされ、このメソッドによってサインアップ・オペレーションの完了処理を行います。

リスト 19. signUp メソッド
def signUp(tokenId:String) = Action.async { implicit request =>
  val id = UUID.fromString(tokenId)
  userTokenService.find(id).flatMap {
    case None => 
      Future.successful(NotFound(views.html.errors.notFound(request)))
    case Some(token) if token.isSignUp && !token.isExpired => 
      userService.find(token.userId).flatMap {
        case None => Future.failed(new IdentityNotFoundException(Messages("error.noUser")))
        case Some(user) => 
          val loginInfo = LoginInfo(CredentialsProvider.ID, token.email)
          for {
            authenticator <- env.authenticatorService.create(loginInfo)
            value <- env.authenticatorService.init(authenticator)
            _ <- userService.confirm(loginInfo)
            _ <- userTokenService.remove(id)
            result <- env.authenticatorService.embed(value, Redirect(routes.Application.index()))
          } yield result
      }
    case Some(token) => 
      userTokenService.remove(id).map {_ => NotFound(views.html.errors.notFound(request))}
  }
}

signUp メソッドはまず、該当するトークン ID がデータベース内に存在するかどうかを確認し、トークン ID がデータベース内に保管されていない場合は、アプリケーションの Not Found エラー・ページにユーザーをリダイレクトします。次に signUp メソッドはトークン ID がサインアップ・トークンと一致すること、そのトークンが失効していないこと、そのトークンに関連付けられているユーザーが存在していることを検証します。すべての検証結果に問題がなければ、コードはサインアップ・プロセスの完了処理、そしてユーザーのサインイン処理へと進みます。サインアップ・プロセスは、ユーザーがサインアップを確認したことを記録してサインアップ・トークンを削除することによって完了します。ユーザーのサインインは、次の 3 つのステップで行われます。

  1. env.authenticatorService.create を呼び出して、認証子 (認証済みユーザーのデータを記録するトークン。「Silhouette の概要」セクションの説明を参照) を作成します。
  2. 認証子を初期化します (env.authenticatorService.init)。
  3. 認証子をリクエスト・ハンドラーのレスポンスに埋め込み、index ページにリダイレクトします (env.authenticatorService.embed)。

このシーケンスによって、サインアップ・プロセスが完了します。この 3 つのステップも、認証コードに含まれています。

資格情報による認証

非同期 authenticate メソッド (リスト 20 を参照) は、サインアップ・プロセスで定義された資格情報を使用してユーザーを認証するためのロジックを実装します。

リスト 20. authenticate メソッド
def authenticate = Action.async { implicit request =>
  signInForm.bindFromRequest.fold(
    bogusForm => Future.successful(
      BadRequest(views.html.auth.signIn(bogusForm, socialProviderRegistry))),
    signInData => {
      val credentials = Credentials(signInData.email, signInData.password)
      credentialsProvider.authenticate(credentials).flatMap { loginInfo => 
        userService.retrieve(loginInfo).flatMap {
          case None => 
            Future.successful(Redirect(routes.Auth.signIn())
              .flashing("error" -> Messages("error.noUser")))
          case Some(user) if !user.profileFor(loginInfo).map(_.confirmed).getOrElse(false) =>
            Future.successful(Redirect(routes.Auth.signIn())
              .flashing("error" -> Messages("error.unregistered", signInData.email)))
          case Some(_) => for {
            authenticator <- env.authenticatorService.create(loginInfo).map { 
              case authenticator if signInData.rememberMe => authenticator.copy(...) // Extend lifetime
              case authenticator => authenticator
            }
            value <- env.authenticatorService.init(authenticator)
            result <- env.authenticatorService.embed(value, Redirect(routes.Application.index()))
          } yield result
        }
      }.recover {
        case e:ProviderException => 
          Redirect(routes.Auth.signIn()).flashing("error" -> Messages("error.invalidCredentials"))
      }
    }
  )
}

authenticate メソッドは、図 2 に示すサインイン・ページから呼び出されます。

図 2. サインイン・ページ
ユーザーに e-メール・アドレスとパスワードの入力を求める、OAuth1 認証用 Twitter アイコンが表示されたサインイン・ページのスクリーンショット
ユーザーに e-メール・アドレスとパスワードの入力を求める、OAuth1 認証用 Twitter アイコンが表示されたサインイン・ページのスクリーンショット

authenticate メソッドのロジックは、見た目よりも単純です。例の如く、このメソッドはリクエストのペイロードを signInForm (e-メール・アドレスとパスワードによるタプル、および remember-me (パスワードを記憶) フラグ) へバインドしようと試みます。フォームが有効でない場合、認証は検証エラーを表示するサインイン・ページへユーザーをリダイレクトして終了します。フォームが有効であれば、認証を試みるために credentialsProvider.authenticate を呼び出します。認証に失敗すると、例外を格納した Future が返されます。コードは、該当するエラー・メッセージをサインイン・ページに表示することにより、例外から回復します。認証に成功すると、redentialsProvider.authenticateLoginInfo インスタンスを格納した Future を返します。ここからは、LoginInfo に関連付けられているユーザーの存在がチェックされ、存在する場合には、そのユーザーが登録を完了済みであるかどうかがチェックされます。これらのチェックに合格すると、コードはリスト 19 に示されている 3 つのステップ、すなわち、認証子の作成、認証子の初期化、レスポンスへの認証子の埋め込み (index ページへのリダイレクト) を実行します。中間ステップとして、「Remember me (パスワードを記憶)」チェック・ボックスが選択されている場合、コードは存続期間を延長したコピーを作成して認証子を変更します (簡単のため、これらの詳細はリストから省いています)。

Twitter を介した認証

ユーザーがサインイン・ページの左下にある Twitter アイコンをクリックした場合、あるいは認証済みユーザーがユーザー・プロファイル・ページ (図 3 を参照) の「Available authentication providers (利用可能な認証プロバイダー)」セクションから Twitter を選択した場合は、Twitter OAuth1 認証が行われます。

図 3. プロファイル・ページ (認証済みユーザーにのみ表示されます)
認証済みユーザーのプロファイル情報と、ユーザーが自分のアカウントにリンクできるプロバイダーが一覧表示された、プロファイル・ページのスクリーンショット
認証済みユーザーのプロファイル情報と、ユーザーが自分のアカウントにリンクできるプロバイダーが一覧表示された、プロファイル・ページのスクリーンショット

いずれの場合も、アプリケーションは providerIdtwitter ストリングに設定したリクエストを /auth/social/:providerId ルートに送信します。リスト 21 に、このルートに関連付けられた socialAuthenticate メソッドを記載します。

リスト 21. Twitter 認証
def socialAuthenticate(providerId:String) = UserAwareAction.async { implicit request =>
  (socialProviderRegistry.get[SocialProvider](providerId) match {
    case Some(p:SocialProvider with CommonSocialProfileBuilder) => p.authenticate.flatMap {
      case Left(result) => Future.successful(result)
      case Right(authInfo) => for {
        profile <- p.retrieveProfile(authInfo)
        user <- request.identity.fold(userService.save(profile))(userService.link(_,profile))
        authInfo <- authInfoRepository.save(profile.loginInfo, authInfo)
        authenticator <- env.authenticatorService.create(profile.loginInfo)
        value <- env.authenticatorService.init(authenticator)
        result <- env.authenticatorService.embed(value, Redirect(routes.Application.index()))
      } yield result
    } 
    case _ => Future.successful(
      Redirect(request.identity.fold(routes.Auth.signIn())(_ => routes.Application.profile()))
	    .flashing("error" -> Messages("error.noProvider", providerId))
    )
  }).recover {
    case e:ProviderException => 
      logger.error("Provider error", e)
      Redirect(request.identity.fold(routes.Auth.signIn())(_ => routes.Application.profile()))
        .flashing("error" -> Messages("error.notAuthenticated", providerId))
  }
}

リクエスト・ハンドラーはユーザーを認識します。そのロジックは以下の前提に従っており、リクエストが匿名の場合は、サインイン・オペレーションが行われます。匿名でない場合、リクエストはユーザー・プロファイル・ページからトリガーされたことになるため、アカウントの関連付けオペレーションが行われます。コードはプロバイダー ID がソーシャル・プロバイダー・レジストリーに登録されていることの確認を開始します。登録されていない場合、メソッドはリクエストがサインイン・オペレーションを示しているのか、それともアカウントの関連付けオペレーションを示しているのかによって、サインイン・ページまたはプロファイル・ページにリダイレクトします。この確認が終わった後、コードはプロバイダーの authenticate メソッドを呼び出し、認証処理が開始されます。

  1. authenticate 呼び出しが初めて完了すると、Left(result) 値が返されます。この result は、認証プロセスを実行する外部サイトへのリダイレクトです。外部サイトは認証を完了すると、silhouette.conf 内で構成されているコールバック URL によってアプリケーションに戻ります。リスト 14 に示されているように、コールバックは socialAuthenticate メソッドに関連付けられているルートを指すため、このメソッドが再度実行されます。
  2. socialAuthenticate が再度実行されると、このメソッドは再びプロバイダーの authenticate メソッドを呼び出しますが、この場合は Right(authInfo) 値を返します。この値は、以下のように処理されます。
    1. アプリケーションは取得した authInfo に関連付けられている身元識別プロファイルをソーシャル・プロバイダーに要求します。
    2. アプリケーションは、リクエストが匿名であるかどうかをチェックします。匿名の場合、サインイン・オペレーションであると見なされて、ユーザーが作成されるか更新されます (リスト 9 を参照)。匿名でない場合、ユーザーにはアカウントが関連付けられているため、身元識別プロファイルは既存のユーザーに関連付けられます。
    3. コードは authInfo を保存して、リスト 19 に示されている通常の 3 つのステップ、すなわち、認証子の作成、認証子の初期化、レスポンスへの認証子の埋め込み (index ページへのリダイレクト) を実行します。

OAuth1 を実行中に authenticate の呼び出しが失敗すると、このメソッドはユーザーを index ページまたはユーザー・プロファイル・ページにリダイレクトすることによって回復します。

REST API 呼び出しをセキュアにする

セキュリティー保護されているリソースに対して、認証されていないユーザーがアクセスを試行すると、アプリケーションはそのリクエストを index ページへとリダイレクトします (「アプリケーションの構成」セクションを参照)。ただし、このエラー・ポリシーは、REST API エンドポイントをセキュアにする上では許容されません。REST API エラー・レスポンスでは、ユーザーをアプリケーションのページにリダイレクトするのではなく、レスポンスに適切な HTTP ステータスと、エラー条件を説明するペイロードを含める必要があります。従って、REST API リクエスト・ハンドラーはデフォルトのエラー・ポリシーを上書きしなければなりません。Silhouette を使用すれば、このタスクは容易に行えます。Silhouette トレイトの中に混在するコントローラーは、デフォルトでは何の効果もないエラー・ハンドラーを継承しますが、これらのコントローラーを上書きして、コントローラー・ベースでエラー処理をカスタマイズすることができるためです。リスト 22 に、このアプリケーションの REST API コントローラーのコードを記載します。

リスト 22. REST API コントローラー
class RestApi @Inject() (
  val messagesApi: MessagesApi, 
  val env:Environment[User,CookieAuthenticator]) extends Silhouette[User,CookieAuthenticator] {

  def profile = SecuredAction.async { implicit request =>
    val json = Json.toJson(request.identity.profileFor(request.authenticator.loginInfo).get)
    val prunedJson = json.transform(
      (__ \ 'loginInfo).json.prune andThen 
      (__ \ 'passordInfo).json.prune andThen 
      (__ \ 'oauth1Info).json.prune)
    prunedJson.fold(
      _ => Future.successful(InternalServerError(Json.obj("error" -> Messages("error.profileError")))),
      js => Future.successful(Ok(js))
    )
  }

  override def onNotAuthenticated(request:RequestHeader) = {
    Some(Future.successful(Unauthorized(Json.obj("error" -> Messages("error.profileUnauth")))))
  }
}

REST API は単一のセキュア・リクエスト・ハンドラーからなり、このハンドラーはログイン・ユーザーのプロファイル情報が格納された JSON オブジェクトを返します。この JSON オブジェクトは機密情報が削除されてから、呼び出し側に送信されます。このクラスは onNonAuthenticated メソッドをオーバーライドするため、REST API に対する匿名呼び出しは、未承認のステータスと、エラー・メッセージのペイロードが含まれるレスポンスを返します。

まとめ

このチュートリアルでは、ユーザーの管理と認証を実装する、基本的ながらも完全な Play アプリケーションをセットアップする方法を説明しました。最小限のローカライズされた変更 (例えば、MongoDB 以外の永続化メカニズムを使用するために独自の DAO を実装するなど) を加えることで、ユーザー管理をゼロから実装することなく、このアプリケーションを独自のプロジェクトに適用することができます。説明のかたわら、Play がユーザーである開発者に提供しているユーザー・フレンドリーな機能のいくつかのコードを紹介しました。例えば、フェイク・アプリケーションを使用した DAO のテスト、クラスと JSON との間の自動変換、自動でのフォームのバインディングおよび検証、などの機能です。

Play アプリケーションは Bluemix 上で実行できることをご存知でしたか?このシリーズの次回のチュートリアルでは、認証アプリケーションを IBM のクラウドにデプロイする方法を説明するので、引き続きお読みください。


ダウンロード可能なリソース


関連トピック

  • LinkedIn、Klout、Coursera: これらの企業が Scala と Play Framework を採用した理由を調べてください。
  • Play のマニュアル: Play Framework の概要については、「Getting Started」セクションを読んで下さい。
  • Silhouette: OAuth1、OAuth2、OpenID、そして資格情報による認証方式をサポートする Play Framework アプリケーション用認証ライブラリーである、Silhouette の Web サイトにアクセスしてください。
  • specs2: この人気の高い、仕様ベースの Scala 向けテスティング・フレームワークを調べてください。
  • Google Guice: Play のデフォルトの依存性注入実装を詳しく調べてください。
  • developerWorks の Web development ゾーンでは多種多様なリソースを提供しています。
  • Twitter で developerWorks をフォローしてください
  • Play Framework: Play Framework をダウンロードして、独自のリアクティブ・アプリケーションの実装を開始してください。
  • MongoDB: JSON に似たドキュメントを効率的に保管、照会、操作するように設計された NoSQL データベースである、MondoDB を入手してください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Java technology, セキュリティ, Open source
ArticleID=1025564
ArticleTitle=Play を使った Web アプリケーション開発: 第 1 回: Play Framework と Scala でユーザー認証を管理する
publish-date=01212016