LoginSignup
6
5

More than 3 years have passed since last update.

Scala + PlayFramework + pac4j での GitHub ログインの実装

Last updated at Posted at 2019-08-13

諸事情あって PlayFramework における GitHub OAuth を用いた認可の実装方法について調べていたのですが、Play Framework に関する日本語記事が非常に少なく、あっても Java 向けに書かれていたりしたので、改めて自分でまとめてみました。

何か誤り等あればご指摘いただけると助かります。

作ったもの

以下のような画面がトップページに出ます。
logged_out

Login をクリックすると GitHub の画面が開き、アプリの認可が求められます。認可すると

logged_in

こんな感じでログインできます。
「notsecret page」はログインしていなくてもアクセスできて、「secret page」はログインしていないとアクセスできません。

画像からも分かる通り、Herokuに https://scala-github-oauth-demo.herokuapp.com/ として push されており、元リポジトリは https://github.com/HelloRusk/scala-play-github-oauth-demo です。

実装方針

pac4j という Java/Scala 向けに認証認可全般を扱えるライブラリがあり、これの PlayFramework 向けの play-pac4j を使います。具体的にどう実装していくかは play-pac4j のリポジトリの wiki を見るのが一番分かりやすいです。
また、play-pac4j-scala-demo というデモ用リポジトリがありますが、あらゆる種類の認可がごちゃ混ぜになっているので、初めて見ると結構混乱すると思います。

以下、実装の手順を順を追って説明します。

1. build.sbt, application.conf の修正

sbt new playframework/play-scala-seed.g8 でテンプレートを用意した後、設定関係のファイルを修正します。

build.sbt
name := "scala-github-oauth-demo"

version := "1.0-SNAPSHOT"

lazy val root = (project in file(".")).enablePlugins(PlayScala)

scalaVersion := "2.12.7"

val playPac4jVersion = "8.0.0"
val pac4jVersion = "3.7.0"

libraryDependencies += guice
libraryDependencies += ehcache
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "4.0.3" % Test
libraryDependencies += "org.pac4j" %% "play-pac4j" % playPac4jVersion
libraryDependencies += "org.pac4j" % "pac4j-oauth" % pac4jVersion
conf/application.conf
demoapp.base_url = "http://localhost:9000"
demoapp.client_id = "XXXXXXXXXXXXXXXX"
demoapp.client_secret = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"

application.conf には GitHub の認証鍵を書きますが、直書きを避けたい場合は環境変数を使うこともできます

2. SecurityModule の実装

認証認可によってページを保護する仕組みの大元を提供するモジュールを実装します。ドキュメントはこちら

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
import org.pac4j.play.scala.{DefaultSecurityComponents, SecurityComponents}
import org.pac4j.play.{CallbackController, LogoutController}
import org.pac4j.play.store.{PlayCacheSessionStore, PlaySessionStore}
import org.pac4j.play.http.PlayHttpActionAdapter
import play.api.{Configuration, Environment}

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

  val base_url: String = configuration.get[String]("demoapp.base_url")

  override def configure(): Unit = {
    bind(classOf[PlaySessionStore]).to(classOf[PlayCacheSessionStore])
    bind(classOf[SecurityComponents]).to(classOf[DefaultSecurityComponents])

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

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

  @Provides
  def provideGithubClient: GitHubClient = new GitHubClient(
    configuration.get[String]("demoapp.client_id"),
    configuration.get[String]("demoapp.client_secret")
  )

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

Google Guice という DI フレームワークを使っています。

configure() は全般的な設定部分になっています。Play cache に認証情報を保持することや、デフォルトの SecurityComponents を持たせることを決めています。また callbackControllerlogoutController の設定は重要ですが、これは CallbackControllerのwikiLogoutControllerのwiki にそれぞれ説明を譲ります。

provideGithubClient 以降は GitHub を用いた認可の設定をしています。

SecurityModule の実装に合わせて、application.conf に依存性注入をします。

conf/application.conf
+ play.modules.enabled += "modules.SecurityModule"

  demoapp.base_url = "http://localhost:9000"
  demoapp.client_id = "XXXXXXXXXXXXXXXX"
  demoapp.client_secret = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"

3. API の実装

SecurityModule によってコールバックとログアウトのAPIは自動的に用意されますが、ログインのAPIも必要でしょう。そのほか、認可によって得られた情報(ログインした GitHub IDなど)を用いたページの実装の仕方など、アプリケーション層の部分を考えていきます。
ドキュメントはこちらこちらなど

app/controllers/Application.scala
package controllers

import javax.inject.Inject
import org.pac4j.core.client.IndirectClient
import org.pac4j.core.credentials.Credentials
import org.pac4j.core.profile.{CommonProfile, ProfileManager}
import org.pac4j.play.PlayWebContext
import org.pac4j.play.scala.{Security, SecurityComponents}
import play.api.mvc.{RequestHeader, Session}

import scala.collection.JavaConverters._
import scala.compat.java8.OptionConverters._

class Application @Inject() (val controllerComponents: SecurityComponents) extends Security[CommonProfile] {
  private def getProfile(implicit request: RequestHeader): Option[CommonProfile] = {
    val webContext = new PlayWebContext(request, playSessionStore)
    val profileManager = new ProfileManager[CommonProfile](webContext)
    val profile = profileManager.get(true)
    profile.asScala
  }

  def index = Action { implicit request =>
    Ok(views.html.index(getProfile(request)))
  }

  def notSecret = Action { implicit request =>
    Ok(views.html.notsecret())
  }
  def secret = Secure("GitHubClient") { implicit request =>
    Ok(views.html.secret())
  }

  def login = Action { request =>
    val context: PlayWebContext = new PlayWebContext(request, playSessionStore)
    val client = config.getClients.findClient("GitHubClient").asInstanceOf[IndirectClient[Credentials,CommonProfile]]
    val location = client.getRedirectAction(context).getLocation
    val newSession = new Session(mapAsScalaMap(context.getJavaSession).toMap)
    Redirect(location).withSession(newSession)
  }
}

getProfile() は認可が済んでいる場合に、その認可済みユーザーのプロフィールを返す関数です。Scala のパターンマッチを使うために Option 型にしています。

次に、secretnotSecret の違いに注目してください。secret の方は Secure("GitHubClient") となっていますが、このように書くことで secret() を呼び出す API は GitHub の認可が必要であると定義できます。
login の実装はドキュメンテーションにあるものをほぼそのまま踏襲していますが、ここでは "GitHubClient" で種類を決め打ちしている部分が異なります。

routesも修正しておきましょう。

conf/routes
  # An example controller showing a sample home page
- GET     /                           controllers.HomeController.index
+ GET     /                           controllers.Application.index()
+
+ GET     /login                      controllers.Application.login()
+ GET     /logout                     @org.pac4j.play.LogoutController.logout()
+ GET     /oauth_callback             @org.pac4j.play.CallbackController.callback()
+
+ GET     /notsecret                  controllers.Application.notSecret()
+ GET     /secret                     controllers.Application.secret()
+ 
  # Map static resources from the /public folder to the /assets URL path
  GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

4. views の実装

あとは、フロントの twirl テンプレートを適切に実装します。

app/views/notsecret.scala.html
@()

@main("NotSecret Page") {
  <p>Everyone can view this page.</p>
  <p><a href="/">home</a></p>
}
app/view/secret.scala.html
@()

@main("Secret Page") {
  <p>Only logged-in users can view this page.</p>
  <p><a href="/">home</a></p>
}

notsecret.scala.htmlsecret.scala.html は特に難しい点はありません。

app/view/index.scala.html
@(profile: Option[org.pac4j.core.profile.CommonProfile])

@main("Scala GitHub OAuth Demo") {
  <h1>Scala GitHub OAuth Demo</h1>
  @profile match {
    case Some(profile) => {
      <p>You are currently logged in as @profile.getUsername()</p>
      <p><a href="/logout">Logout</a></p>
    }
    case None => {
      <p><a href="/login">Login</a></p>
    }
  }
  <br/>
  <p><a href="/notsecret">notsecret page</a></p>
  <p><a href="/secret">secret page</a></p>
}

index.scala.html の方ですが、認可を済ませていない場合といる場合で表示を変えるために分岐させています。

まず、引数のprofileは認可によって得られたプロフィールの情報 org.pac4j.core.profile.CommonProfileOption で包んだものです。
認可が済んでいる場合は中身にプロフィール情報が含まれていて、認可が済んでいない場合は None です。

ちなみに Common というのはどういうことかというと、GitHub の他にも Twitter認可、Facebook認可、Google認可...など様々な認可がありますが、それらに共通してあるようなユーザープロフィール情報を持っているということです。.getUsername() 以外のメソッドについてはドキュメントを見ていただくと良いかと思います。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5