LoginSignup
9
2

More than 1 year has passed since last update.

SilhouetteからPAC4Jに乗り換えてついでにOpenID Connectを使う

Last updated at Posted at 2021-12-04

はじめに

 Play Frameworkで使えるアカウント認証ライブラリSilhouetteが、2021年9月に今後一切のアップデートを停止し、アーカイブされてしまいました。メインでコミットされていた方曰く1

Hi, sorry for the current situation. In the last year I had no time to maintain the project. Therefore I have decided to step back as a maintainer and archive the project. The project can be forked by the community if there is an interest. The documentation is still available under the URL: https://silhouette.readme.io

面倒を見る時間がなくなってしまったそうです。仕方ありませんね。

 Silhouetteにはとてもお世話になっていたため、早々に認証周りの実装を換装していかなければなりません。そこで換装先として注目したのが、JAVA製セキュリティエンジンのPAC4Jです。

PAC4JはPlayを含む様々なフレームワークで利用できる上、公式にサンプルアプリをオープンソースで公開しており、非常にとっつきやすい製品です。対応している認証方法も多く、組織的にアカウント認証を一元管理する動きがあったこともあり、せっかくなのでOpenID Connect(OIDC)も使ってみることにしました。そんなわけで、Silhouette+DB認証からPAC4J+OIDC認証に切り替えた話をします。

方針

 大きく分けて2段階の作業を行います。

  • SilhouetteのモジュールをPAC4Jのモジュールに置き換える
  • OIDCの各種設定

 モジュール置き換えにあたっては、OIDCの導入によりアーキテクチャが変わります。この点について触れておきましょう。
 従来のシステムでは、ユーザ情報を内部のDBに持ち、認証認可処理もSilhouetteを利用しながら内部に実装していました。
DBauth2
OIDCを使うと、下図のようにユーザ情報や認証認可処理を全て外部サービスに託す形になります。外部サービスへのリダイレクトや、ユーザ情報からのセッション生成をPAC4Jが担います。
OIDCauth3
OIDCにPAC4Jがどのように絡むかは公式の説明図がわかりやすいので、以下に引用します4
図でCASと書かれているものは、IDプロバイダ兼認証サーバです。実例としてはGoogleやOktaなどが当てはまります。
sequence_diagram.jpg
プロセスを書き出すと次の通りです。

  1. ユーザが、認証が必要なページにアクセスする
  2. 外部サイト(CAS)のログインページにリダイレクトする
  3. ユーザが、外部サイトにログインする
  4. 外部サイトから返された認証結果を受け取る
  5. 認証結果からセッションを生成する
  6. ページを表示する

上記の1,3以外、ユーザ情報の参照・認証・認可は実装せずとも全てPAC4Jがやってくれます。ログインフォームも要りません。
逆に言うと、Silhouetteを使って実装していたユーザ情報の参照・認証・認可機能やログインフォームは全て不要となります。

実装

環境は次の通りです。

  • scala: 2.12.11
  • play: 2.8.7
  • play-silhouette: 7.0.0
  • play-pac4j: 11.0.0-PLAY2.8
  • pac4j-http: 5.1.3
  • pac4j-oidc: 5.1.3
  • scala-guice: 4.2.6

モジュールの置き換え

 モジュールを置き換える必要があるのは、要認証ページの表示部分です。例として、Silhouetteを利用した次のようなコントローラを書き換えてみます。ただし、CustomEnvcom.mohiva.play.silhouette.api.Envを継承したトレイト、UserServicecom.mohiva.play.silhouette.api.repositoriesの実装クラス、ErrorHandlercom.mohiva.play.silhouette.api.actions.SecuredErrorHandlerの実装クラスとします。

controllers/ProtectedPageController.scala
import com.google.inject.Inject
import models.CustomEnv
import services.UserService
import services.ErrorHandler
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}

class ProtectedPageController @Inject() (
    cc: ControllerComponents,
    silhouette: Silhouette[CustomEnv],
    userService: UserService,
    errorHandler: ErrorHandler
)(implicit val ec: ExecutionContext) extends AbstractController(cc) {
  def index: Action[AnyContent] = silhouette.SecuredAction(userService)(errorHandler) { implicit req =>
    val user = req.identity
    Ok(views.html.protected(user))
  }
}

これを次のようにPAC4Jを利用した形に置き換えます。

controllers/ProtectedPageController.scala
import com.google.inject.Inject
import org.pac4j.core.profile.UserProfile
import org.pac4j.play.scala.{Security, SecurityComponents}
import play.api.mvc.{Action, AnyContent}

class ProtectedPageController @Inject() (
    val controllerComponents: SecurityComponents,
)(implicit val ec: ExecutionContext) extends Security[UserProfile] {
  def index: Action[AnyContent] = Secure("OidcClient") { implicit req =>
    val user = req.profiles
    Ok(views.html.protected(user))
  }
}

userの型がSilhouetteとPAC4Jで異なっていますが、ここでは問題にしていません。実際に置き換える際はうまく吸収してください。

 ポイントは2点です。

  • val controllerComponents: SecurityComponents
    • 親クラスがPAC4JのSecurity[UserProfile]になったことで登場しています。必ずこの形で書かないとoverrideされないため怒られてしまいます。気をつけましょう。
  • Secure("OidcClient")
    • 親クラスのメソッドで、この中で認証認可処理が行われます。方針の章で挙げたOIDCのプロセスで言うと、2番から5番までのステップを全て実行します。OidcClientはこの後登場するので、字面だけ覚えておきましょう。

ここまでに説明した通り、上記の置き換えで認証認可処理が全て終わってしまいました。よって手元にユーザ情報を保持する必要がなくなりましたので、従来利用していたSilhouetteモジュールをはじめ、ユーザ情報のCRUD処理やデータモデルも全て不要となります。削除しておきましょう。

OIDCの各種設定

外部サイト(CAS)側の設定

CASの公式ドキュメント等に従い、アプリケーションの登録を済ませましょう。
例としてOktaのドキュメントを掲載しておきます。

次の3つの設定値はアプリケーションの設定にも加えるため、控えておきましょう。

  • Sign-in redirect URIs(自分で設定)
  • ClientId(自動生成)
  • ClientSecret(自動生成)

アプリケーションの設定

 この辺りは最早公式のデモリポジトリ(GitHub)を見た方が早いのですが、念の為公式リポジトリの中から要点を掻い摘んでお話しします。

 まずは外部サイト(CAS)の設定を反映させましょう。clientIdclientSecretはそのまま記入します。

application.conf
oidc {
  baseUrl = {yourAppHost}
  discoveryUri = "https://{yourCasDomain}/.well-known/openid-configuration"
  clientId = {clientId}
  clientSecret = {clientSecret}
}

 baseUrlにはアプリケーションのホストを記入してください。discoveryUriとは、末尾が.well-known/openid-configurationであるようなURIで、CASのプロバイダ情報を返してくれるエンドポイントです。お使いのCASに固有のエンドポイントが決まっているため、調べて記入しましょう。

 続いてPAC4Jの設定を行うモジュールを作成します。

modules/Pac4JModule.scala
import com.google.inject.{AbstractModule, Provides}
import net.codingwell.scalaguice.ScalaModule
import org.pac4j.core.client.Clients
import org.pac4j.core.client.direct.AnonymousClient
import org.pac4j.core.config.Config
import org.pac4j.core.context.session.SessionStore
import org.pac4j.oidc.client.OidcClient
import org.pac4j.oidc.config.OidcConfiguration
import org.pac4j.play.http.PlayHttpActionAdapter
import org.pac4j.play.{CallbackController, LogoutController}
import org.pac4j.play.scala.{DefaultSecurityComponents, SecurityComponents}
import org.pac4j.play.store.{PlayCookieSessionStore, ShiroAesDataEncrypter}
import play.api.{Configuration, Environment}

class Pac4JModule(environment: Environment, configuration: Configuration) extends AbstractModule with ScalaModule {

  val baseUrl: String = configuration.get[String]("oidc.baseUrl")

  override def configure(): Unit = {
    val playSecretKey    = configuration.get[String]("play.http.secret.key").substring(0, 16)
    val dataEncrypter    = new ShiroAesDataEncrypter(playSecretKey.getBytes)
    val playSessionStore = new PlayCookieSessionStore(dataEncrypter)
    bind(classOf[SessionStore]).toInstance(playSessionStore)
    bind(classOf[SecurityComponents]).to(classOf[DefaultSecurityComponents])

    // CASログイン後の処理(callback)を行うコントローラの設定
    val callbackController = new CallbackController()
    callbackController.setDefaultUrl("/")
    bind(classOf[CallbackController]).toInstance(callbackController)

    // CASログアウト後の処理を行うコントローラの設定
    val logoutController = new LogoutController()
    logoutController.setDefaultUrl("/")
    bind(classOf[LogoutController]).toInstance(logoutController)
  }

  @Provides
  def provideOidcClient: OidcClient = {
    val oidcConfiguration = new OidcConfiguration()
    oidcConfiguration.setDiscoveryURI(configuration.get[String]("oidc.discoveryUri"))
    oidcConfiguration.setClientId(configuration.get[String]("oidc.clientId"))
    oidcConfiguration.setSecret(configuration.get[String]("oidc.clientSecret"))
    val oidcClient = new OidcClient(oidcConfiguration)
    oidcClient
  }

  @Provides
  def provideConfig(oidcClient: OidcClient): Config = {
    // CASで設定した Sign-in redirect URI が生成・設定される
    // この場合は {baseUrl}/callback?client_name=OidcClient というURIになる
    val clients = new Clients(baseUrl + "/callback", oidcClient)
    val config  = new Config(clients)
    config.setHttpActionAdapter(new PlayHttpActionAdapter())
    config
  }
}

DIを有効にするため、設定にも追記しておきましょう。

application.conf
modules {
    enabled += "modules.Pac4JModule"
  }

ここに現れるOidcClientが、モジュールの置き換えで出てきたSecure("OidcClient")の引数を決めています。OidcClientを使って認証認可処理を通す、という意味だったのですね。
 また、順番が前後して申し訳ないのですが、Sign-in redirect URIsも実際にはここで決まっています。思っていたのと違う!となったら、CASの設定をアプリケーションに合わせましょう。

 最後にエンドポイントを設定します。

GET   /index                         @controllers.ProtectedPageController.index()
GET   /callback                      @org.pac4j.play.CallbackController.callback(request: Request)
POST  /callback                      @org.pac4j.play.CallbackController.callback(request: Request)
GET   /logout                        @org.pac4j.play.LogoutController.logout(request: Request)

アクセスしたいページ以外は全てPAC4Jに向けます。ありがたい。

おわりに

 SilhouetteやPAC4Jの話よりもOpenID Connectの話の方が長いので、もしかしてOpenID Connectの話がしたかったのでは?とお思いかもしれませんが、その通りです。ありがとうございました。


  1. Gitter, mohiva/play-silhouette, https://gitter.im/mohiva/play-silhouette?at=61450c78fd7409696e189e6a, viewed at 2021-11-26 

  2. This work is made of a composition of "User icon" by Paomedia and "Computer, essential, app icon" by Just Icon and "Id, user icon" by Paomedia and "Database icon" by Webalys, used under CC BY, this work is licensed under CC BY by ocome. 

  3. This work is made of a composition of "User icon" by Paomedia and "Computer, essential, app icon" by Just Icon and "Id, user icon" by Paomedia and "Key, lock, password, project, protect icon by Artem White, used under CC BY, this work is licensed under CC BY by ocome. 

  4. PAC4J, https://www.pac4j.org/docs/authentication-flows.html, viewed at 2021-11-26 

9
2
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
9
2