Playframework pac4jを使用したTwitter、GitHubソーシャルログイン

個人開発したアプリの宣伝
目的地が設定できる手帳のような使い心地のTODOアプリを公開しています。
Todo with Location

Todo with Location

  • Yoshiko Ichikawa
  • Productivity
  • Free

スポンサードリンク

pac4jとは?

pac4jはrubyでのOmniAuthのようなJavaで書かれた横断的なソーシャルログインライブラリ。

pac4j: security for Java

https://github.com/pac4j/play-pac4j

Scala + Playframeworkでも利用できるので、TwitterとGitHubのソーシャルログインを試した時の実装を記録しておきます。


OAuthプロバイダのアプリ登録

予め、各、OAuthプロバイダにアプリ登録を済ませておき、consumer keyとsecretを取得しておきます。尚、callbackurlは、http://your.domain.com/oauth_callbackのようにoauth_callbackというパスにcallbackされるよう登録しておきます。各プロバイダ事に変更する必要はなく、共通のパスで問題ありません。


依存ライブラリの登録

syncCacheApiを使用するので、ehcacheも必要なので注意。

buiild.sbt

libraryDependencies += ehcache
libraryDependencies += "org.pac4j" %% "play-pac4j" % "10.0.0"
libraryDependencies += "org.pac4j" % "pac4j-oauth" % "4.0.0"


インジェクトするインスタンスの登録

Moduleを作成し、Controllerやpac4jが使用するインスタンス(例えばセッションストアやOAuthのconsumer keyなど)の注入定義を作成します。

実装例: Security configuration · pac4j/play-pac4j Wiki · GitHub

app/modules/SecurityModule.scala

package modules

import com.google.inject.{AbstractModule, Provides}
import org.pac4j.core.client.Clients
import org.pac4j.core.config.Config
import org.pac4j.oauth.client.{GitHubClient, TwitterClient}
import org.pac4j.play.http.PlayHttpActionAdapter
import org.pac4j.play.scala.{DefaultSecurityComponents, SecurityComponents}
import org.pac4j.play.{CallbackController, LogoutController}
import org.pac4j.play.store.{PlayCacheSessionStore, PlaySessionStore}
import play.api.Environment
import play.api.Configuration

class SecurityModule (environment: Environment, configuration: Configuration) extends AbstractModule {

  val baseUrl = "http://localhost:9000"

  override def configure(): Unit = {

    bind(classOf[PlaySessionStore]).to(classOf[PlayCacheSessionStore])

    // callback
    val callbackController = new CallbackController()
    callbackController.setDefaultUrl("/")
    callbackController.setMultiProfile(true)
    bind(classOf[CallbackController]).toInstance(callbackController)

    // logout
    val logoutController = new LogoutController()
    logoutController.setDefaultUrl("/signout")
    bind(classOf[LogoutController]).toInstance(logoutController)

    // security components used in controllers
    bind(classOf[SecurityComponents]).to(classOf[DefaultSecurityComponents])
  }

  @Provides
  def provideGithubClient: GitHubClient = new GitHubClient(
    "Client ID",
    "Client Secret"
  )

  @Provides
  def provideTwitterClient: TwitterClient = new TwitterClient(
    "CONSUMER_KEY",
    "CONSUMER_SECRET"
  )

  @Provides
  def provideConfig(twitterClient: TwitterClient, gitHubClient: GitHubClient):Config = {
    val clients = new Clients(baseUrl + "/oauth_callback", twitterClient, gitHubClient)
    val config = new Config(clients)
    config.setHttpActionAdapter(new PlayHttpActionAdapter())
    config
  }
}


configureメソッド

主にControllerに注入するインスタンスの定義を記載する。

CallbackControllersetDefaultUrlは多分、認証成功時のコールバック後にforwardされるパスだと思うんだけど、後述するController側で認証成功時のコールバックを定義できるので、多分、あまり意味をなさないパラメータじゃないかなと思う。

LogoutControllersetDefaultUrlはログアウト処理はpac4j側のControllerにルーティングする必要があるので、そのログアウト処理が終了した後にforwardするパスを定義する。つまりログアウトしました的な画面が必要な場合や、自身のアプリケーションでのセッションの破棄などが必要な場合、その画面や機能に対応したパスを記述すればいいと思います。


provideメソッド

@Providesアノテーションが付いたメソッドは戻り値を注入インスタンスとして解釈します。多分、戻り値型と注入対象の型が一致した場合とかでのルールだと思う。一応、どこかのページではメソッド名の接頭辞にprovideをつけると書いてあった。

主にpac4jで使用するインスタンスの生成のルールを記述しておきます。

例では、pac4jで利用するGitHubClientTwitterClientのインスタンスを作成してそれを返す実装となります。各インスタンスはOAuthプロバイダから発行されたconsumer keyとsecretをコンストラクタの引数にとります。(Twitterはconsumer key, secretのみで大丈夫のようです)


以下では使用する認証プロバイダの定義を記述しています。使用するプロバイダクライアントのオブジェクトを引数に渡すことで、@Provides定義されたインスタンスが渡されます。

@Provides
def provideConfig(twitterClient: TwitterClient, gitHubClient: GitHubClient):Config = {
  val clients = new Clients(baseUrl + "/oauth_callback", twitterClient, gitHubClient)
  val config = new Config(clients)
  config.setHttpActionAdapter(new PlayHttpActionAdapter())
  config
}

new Clients(baseUrl + "/oauth_callback", twitterClient, gitHubClient)で、認証プロバイダのアプリ登録で行ったcallback URLを設定します。


Moduleのインジェクト定義を有効にする

conf/application.conf

play.modules.enabled += "modules.SecurityModule"


ログインリンクの作成

GitHub、Twitterでのログインリンクとログアウトリンクを配置したテンプレートを記述します。

Plat2-8-Pac4j-Sample/index.scala.html at master · letitride/Plat2-8-Pac4j-Sample · GitHub

app/views/index.scala.html

@()

@main("Welcome to Play") {
  <h1>Welcome to Play!</h1>
  <a href="@controllers.routes.HomeController.githublogin()">GitHub Login</a>
<br />
<a href="@controllers.routes.HomeController.twitterlogin()">Twitter Login</a>
<br />
<a href="/logout">Logout</a>
}

各パスのルーティングの定義

conf/routes

GET     /                           controllers.HomeController.index
GET     /githublogin                controllers.HomeController.githublogin()
GET     /twitterlogin                controllers.HomeController.twitterlogin()
GET     /oauth_callback             @org.pac4j.play.CallbackController.callback(request: Request)
GET     /logout                     @org.pac4j.play.LogoutController.logout(request: Request)
GET     /signout                       controllers.HomeController.signout()

/signoutはSecurityModuleのlogoutController.setDefaultUrl("/signout")で設定したパスとなります。

上記の通り、oauth_callbacklogoutはpac4jで用意されているControllerにルーティングされます。

ソーシャルログインの受付

githublogintwitterloginに対応したメソッドをControllerに記述します。

Plat2-8-Pac4j-Sample/index.scala.html at master · letitride/Plat2-8-Pac4j-Sample · GitHub

package controllers

import java.util.Optional

import javax.inject._
import org.pac4j.core.profile.{CommonProfile, ProfileManager}
import org.pac4j.oauth.profile.github.GitHubProfile
import org.pac4j.play.{PlayWebContext}
import org.pac4j.play.scala.{Security, SecurityComponents}
import play.api.mvc._


/**
 * This controller creates an `Action` to handle HTTP requests to the
 * application's home page.
 */
@Singleton
class HomeController @Inject() (val controllerComponents: SecurityComponents) extends BaseController with Security[CommonProfile] {

  private def getProfile(implicit request: RequestHeader): Optional[CommonProfile] = {
    val webContext = new PlayWebContext(request, playSessionStore)
    val profileManager = new ProfileManager[CommonProfile](webContext)
    val profile = profileManager.get(true)
    profile
  }

  def index() = Action { implicit request: Request[AnyContent] =>
    Ok(views.html.index())
  }

  def twitterlogin() = Secure("TwitterClient"){implicit request =>
    println(getProfile.map{ p => println(p) })
    Ok(views.html.index())
  }

  def githublogin() = Secure("GithubClient"){ implicit request =>
    val og = getProfile.map{ o => o.asInstanceOf[GitHubProfile] }
    println(getProfile map{ p => println(p) })
    Ok(views.html.index())
  }

  def signout() = Action{ implicit request =>
    println("signout")
    println(getProfile map{ p => println(p) })
    Ok(views.html.index())
  }
}

getProfileメソッドはソーシャルログイン済みの場合、プロバイダから返されたユーザProfileを返します。尚、認証成功時のProfileのストアはpac4j側で自動で行ってくれるようです。

twitterlogingithubloginは上記のようにSecureというActionBuilderのapplyメソッドに、対応するプロバイダClient名(文字列なので注意)を渡して実行することで、各プロバイダの認証・認可画面を自動で表示します。その後、プロバイダから、callbackのURLが呼び出されpac4jのcallbackメソッドが実行されます。

また、Secureのapplyメソッドには認証成功時のコールバックメソッドを渡します。これは、pac4jのCallbackControllerから実行されます(なので先述したcallbackController.setDefaultUrl("/")はあまり意味はないのでは...と思いました)。

実装の例では認証プロバイダから取得したProfile情報を出力しています。あとはプロバイダから返されたaccess tokenを使用してサービスAPIを叩いたり、何かしらのユーザ情報をDBに登録等しておけば良いと思います。

実装例: github.com