pac4j Scala
認証ライブラリ
pac4j Scala
概要
pac4j Scalaは、Play Framework向けのセキュリティライブラリで、OAuth、CAS、SAML、OpenID Connectなどの認証をScalaで提供します。
詳細
pac4j Scalaは、Scala言語とPlay Frameworkのためのセキュリティライブラリです。Apache 2ライセンスの下で提供され、pac4jセキュリティエンジンをベースにしたPlay Framework v2のWebアプリケーションおよびWebサービス向けの包括的なセキュリティソリューションです。OAuth(Facebook、Twitter、Google等)、SAML、CAS、OpenID Connect、HTTP、Google App Engine、Kerberos、LDAP、SQL、JWT、MongoDB、CouchDB、IPアドレス、REST APIなど幅広い認証メカニズムをサポートしています。ロール、匿名/記憶/完全認証ユーザー、プロファイルタイプと属性、CORS、CSRF、セキュリティヘッダー、IPアドレス、HTTPメソッド制限などの認可機能も提供します。SecureアノテーションとSecurityトレイトでメソッドを保護し、SecurityFilterでURLを保護できます。Deadboltとの連携も可能で、Play 2.6〜2.8およびScala 2.11〜2.13との互換性があります。
メリット・デメリット
メリット
- Play Framework特化: Play Framework v2との完全統合
- Scala言語サポート: Scalaの型安全性と関数型プログラミング活用
- 多様な認証: OAuth、SAML、OpenID Connect、JWT等幅広い認証メカニズム
- 関数型アプローチ: Scalaの特性を活かした関数型認証処理
- 型安全性: コンパイル時の型チェックによる安全性確保
- 非同期対応: Play Frameworkの非同期処理に最適化
- デモ充実: play-pac4j-scala-demoによる実装例提供
デメリット
- Play限定: Play Framework以外では使用不可
- Scala知識: Scala言語の習熟が必要
- 学習コスト: pac4jとScalaの両方の理解が必要
- コミュニティ規模: Java版と比較してコミュニティが小さい
- バージョン依存: Play/Scalaバージョンとの厳密な依存関係
- ドキュメント: 日本語ドキュメントが限定的
主要リンク
書き方の例
基本的なPlay設定(application.conf)
# application.conf
play.modules.enabled += "org.pac4j.play.PlayModule"
play.http.filters += "org.pac4j.play.filters.SecurityFilter"
# pac4j設定
pac4j {
security {
rules = [
{
"/admin/.*" = {
authorizers = "admin"
clients = "FormClient"
}
},
{
"/.*" = {
authorizers = "isAuthenticated"
clients = "FacebookClient,TwitterClient,GoogleOidcClient"
}
}
]
}
}
SecurityModuleの設定
// modules/SecurityModule.scala
import com.google.inject.{AbstractModule, Provides}
import org.pac4j.core.config.Config
import org.pac4j.core.client.Clients
import org.pac4j.http.client.indirect.FormClient
import org.pac4j.http.credentials.authenticator.test.SimpleTestUsernamePasswordAuthenticator
import org.pac4j.oauth.client.{FacebookClient, TwitterClient}
import org.pac4j.oidc.client.GoogleOidcClient
import org.pac4j.oidc.config.OidcConfiguration
import org.pac4j.play.{CallbackController, LogoutController}
import org.pac4j.play.store.{PlayCookieSessionStore, PlaySessionStore}
import play.api.{Configuration, Environment}
class SecurityModule(environment: Environment, configuration: Configuration)
extends AbstractModule {
override def configure(): Unit = {
bind(classOf[PlaySessionStore]).to(classOf[PlayCookieSessionStore])
}
@Provides
def provideConfig: Config = {
// Facebook OAuth設定
val facebookClient = new FacebookClient(
configuration.get[String]("pac4j.facebook.id"),
configuration.get[String]("pac4j.facebook.secret")
)
// Twitter OAuth設定
val twitterClient = new TwitterClient(
configuration.get[String]("pac4j.twitter.id"),
configuration.get[String]("pac4j.twitter.secret")
)
// Google OpenID Connect設定
val oidcConfig = new OidcConfiguration()
oidcConfig.setClientId(configuration.get[String]("pac4j.google.id"))
oidcConfig.setSecret(configuration.get[String]("pac4j.google.secret"))
oidcConfig.setIssuer("https://accounts.google.com")
val googleOidcClient = new GoogleOidcClient(oidcConfig)
// フォーム認証設定
val formClient = new FormClient("/loginForm",
new SimpleTestUsernamePasswordAuthenticator())
val clients = new Clients("/callback",
facebookClient, twitterClient, googleOidcClient, formClient)
new Config(clients)
}
@Provides
def provideCallbackController(config: Config): CallbackController = {
new CallbackController()
}
@Provides
def provideLogoutController(config: Config): LogoutController = {
new LogoutController()
}
}
コントローラーでの認証制御
// controllers/ApplicationController.scala
import javax.inject.Inject
import org.pac4j.core.profile.{CommonProfile, ProfileManager}
import org.pac4j.play.scala.{Security, SecurityComponents}
import play.api.mvc._
class ApplicationController @Inject()(
val controllerComponents: SecurityComponents
) extends Security[CommonProfile] {
// 認証が必要なアクション
def secure: Action[AnyContent] = Secure("FacebookClient") { implicit request =>
val profile = request.profiles.headOption
val name = profile.map(_.getDisplayName).getOrElse("Unknown")
Ok(views.html.secure(name))
}
// 複数クライアント対応
def multiAuth: Action[AnyContent] =
Secure("FacebookClient,TwitterClient,GoogleOidcClient") { implicit request =>
val profiles = request.profiles
Ok(views.html.multiAuth(profiles))
}
// 管理者権限が必要
def admin: Action[AnyContent] =
Secure("FormClient", "admin") { implicit request =>
Ok(views.html.admin())
}
// カスタム認可
def customAuth: Action[AnyContent] =
Secure("FacebookClient", "custom") { implicit request =>
Ok("Custom authorized content")
}
// プロファイル情報表示
def profile: Action[AnyContent] = Secure("FacebookClient") { implicit request =>
val profile = request.profiles.head
val attributes = profile.getAttributes.asScala.toMap
Ok(views.html.profile(profile, attributes))
}
}
カスタムオーソライザー
// security/CustomAuthorizer.scala
import org.pac4j.core.authorization.authorizer.ProfileAuthorizer
import org.pac4j.core.context.WebContext
import org.pac4j.core.profile.CommonProfile
import scala.jdk.CollectionConverters._
class CustomAuthorizer extends ProfileAuthorizer[CommonProfile] {
override def isAuthorized(context: WebContext,
profiles: java.util.List[CommonProfile]): Boolean = {
val scalaProfiles = profiles.asScala.toList
scalaProfiles.headOption match {
case Some(profile) =>
// カスタム認可ロジック
profile.getAttribute("department") match {
case dept: String if dept == "Engineering" => true
case _ => false
}
case None => false
}
}
}
// SecurityModule.scalaでの登録
@Provides
def provideConfig: Config = {
// ... clients設定 ...
val config = new Config(clients)
config.addAuthorizer("custom", new CustomAuthorizer())
config
}
非同期アクションでの認証
// controllers/AsyncController.scala
import scala.concurrent.{ExecutionContext, Future}
import org.pac4j.core.profile.CommonProfile
import org.pac4j.play.scala.Security
class AsyncController @Inject()(
val controllerComponents: SecurityComponents
)(implicit ec: ExecutionContext) extends Security[CommonProfile] {
def asyncSecure: Action[AnyContent] =
Secure("FacebookClient").async { implicit request =>
Future {
val profile = request.profiles.head
val userId = profile.getId
// 非同期処理
processUserData(userId).map { result =>
Ok(s"Processed: $result")
}
}.flatten
}
def processUserData(userId: String): Future[String] = {
// データベースアクセスなどの非同期処理
Future.successful(s"User data for $userId")
}
}
ユーザープロファイル管理
// services/ProfileService.scala
import org.pac4j.core.profile.CommonProfile
import play.api.libs.json._
import scala.jdk.CollectionConverters._
class ProfileService {
def extractUserInfo(profile: CommonProfile): UserInfo = {
UserInfo(
id = profile.getId,
displayName = profile.getDisplayName,
email = Option(profile.getAttribute("email")).map(_.toString),
firstName = Option(profile.getFirstName),
familyName = Option(profile.getFamilyName),
roles = profile.getRoles.asScala.toSet,
attributes = profile.getAttributes.asScala.toMap
)
}
def toJson(userInfo: UserInfo): JsValue = {
Json.obj(
"id" -> userInfo.id,
"displayName" -> userInfo.displayName,
"email" -> userInfo.email,
"firstName" -> userInfo.firstName,
"familyName" -> userInfo.familyName,
"roles" -> userInfo.roles,
"attributes" -> userInfo.attributes
)
}
}
case class UserInfo(
id: String,
displayName: String,
email: Option[String],
firstName: Option[String],
familyName: Option[String],
roles: Set[String],
attributes: Map[String, AnyRef]
)
WebSocketでの認証
// controllers/WebSocketController.scala
import akka.actor.ActorSystem
import akka.stream.Materializer
import org.pac4j.core.profile.CommonProfile
import play.api.libs.streams.ActorFlow
import play.api.mvc.WebSocket
import actors.WebSocketActor
class WebSocketController @Inject()(
val controllerComponents: SecurityComponents
)(implicit system: ActorSystem, mat: Materializer)
extends Security[CommonProfile] {
def socket: WebSocket =
WebSocket.acceptOrResult[String, String] { implicit request =>
Future {
val profileManager = new ProfileManager[CommonProfile](request.webContext)
profileManager.get(true).asScala.headOption match {
case Some(profile) =>
Right(ActorFlow.actorRef { out =>
WebSocketActor.props(out, profile)
})
case None =>
Left(Forbidden("Authentication required"))
}
}
}
}