pac4jとは?
pac4jはrubyでのOmniAuthのような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に注入するインスタンスの定義を記載する。
CallbackController
のsetDefaultUrl
は多分、認証成功時のコールバック後にforwardされるパスだと思うんだけど、後述するController側で認証成功時のコールバックを定義できるので、多分、あまり意味をなさないパラメータじゃないかなと思う。
LogoutController
のsetDefaultUrl
はログアウト処理はpac4j側のControllerにルーティングする必要があるので、そのログアウト処理が終了した後にforwardするパスを定義する。つまりログアウトしました
的な画面が必要な場合や、自身のアプリケーションでのセッションの破棄などが必要な場合、その画面や機能に対応したパスを記述すればいいと思います。
provideメソッド
@Provides
アノテーションが付いたメソッドは戻り値を注入インスタンスとして解釈します。多分、戻り値型と注入対象の型が一致した場合とかでのルールだと思う。一応、どこかのページではメソッド名の接頭辞にprovide
をつけると書いてあった。
主にpac4jで使用するインスタンスの生成のルールを記述しておきます。
例では、pac4jで利用するGitHubClient
、TwitterClient
のインスタンスを作成してそれを返す実装となります。各インスタンスは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_callback
とlogout
はpac4jで用意されているControllerにルーティングされます。
ソーシャルログインの受付
githublogin
、twitterlogin
に対応したメソッドを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側で自動で行ってくれるようです。
twitterlogin
、githublogin
は上記のようにSecure
というActionBuilderのapplyメソッドに、対応するプロバイダClient名(文字列なので注意)を渡して実行することで、各プロバイダの認証・認可画面を自動で表示します。その後、プロバイダから、callbackのURLが呼び出されpac4jのcallbackメソッドが実行されます。
また、Secureのapplyメソッドには認証成功時のコールバックメソッドを渡します。これは、pac4jのCallbackControllerから実行されます(なので先述したcallbackController.setDefaultUrl("/")
はあまり意味はないのでは...と思いました)。
実装の例では認証プロバイダから取得したProfile情報を出力しています。あとはプロバイダから返されたaccess tokenを使用してサービスAPIを叩いたり、何かしらのユーザ情報をDBに登録等しておけば良いと思います。
実装例: github.com