pac4j Scala
Authentication Library
pac4j Scala
Overview
pac4j Scala is a security library for Play Framework that provides OAuth, CAS, SAML, OpenID Connect and other authentication mechanisms in Scala.
Details
pac4j Scala is a security library for the Scala language and Play Framework. Provided under the Apache 2 license, it's a comprehensive security solution for Play Framework v2 web applications and web services based on the pac4j security engine. It supports a wide range of authentication mechanisms including OAuth (Facebook, Twitter, Google, etc.), SAML, CAS, OpenID Connect, HTTP, Google App Engine, Kerberos, LDAP, SQL, JWT, MongoDB, CouchDB, IP address, and REST API. It also provides authorization features including roles, anonymous/remember-me/fully authenticated users, profile type and attributes, CORS, CSRF, security headers, IP address, and HTTP method restrictions. The Secure annotation and Security trait protect methods, while SecurityFilter protects URLs. It can work with Deadbolt and is compatible with Play 2.6-2.8 and Scala 2.11-2.13.
Pros and Cons
Pros
- Play Framework Specialized: Complete integration with Play Framework v2
- Scala Language Support: Leverages Scala's type safety and functional programming
- Diverse Authentication: Wide range of authentication mechanisms like OAuth, SAML, OpenID Connect, JWT
- Functional Approach: Functional authentication processing utilizing Scala characteristics
- Type Safety: Safety ensured through compile-time type checking
- Async Support: Optimized for Play Framework's asynchronous processing
- Rich Demos: Implementation examples provided through play-pac4j-scala-demo
Cons
- Play Limited: Cannot be used outside Play Framework
- Scala Knowledge: Requires proficiency in Scala language
- Learning Curve: Requires understanding of both pac4j and Scala
- Community Size: Smaller community compared to Java version
- Version Dependencies: Strict dependency relationships with Play/Scala versions
- Documentation: Limited Japanese documentation
Key Links
- play-pac4j GitHub
- play-pac4j Scaladex
- play-pac4j-scala-demo
- pac4j Official Site
- Play Framework Documentation
Code Examples
Basic Play Configuration (application.conf)
# application.conf
play.modules.enabled += "org.pac4j.play.PlayModule"
play.http.filters += "org.pac4j.play.filters.SecurityFilter"
# pac4j configuration
pac4j {
security {
rules = [
{
"/admin/.*" = {
authorizers = "admin"
clients = "FormClient"
}
},
{
"/.*" = {
authorizers = "isAuthenticated"
clients = "FacebookClient,TwitterClient,GoogleOidcClient"
}
}
]
}
}
SecurityModule Configuration
// 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 configuration
val facebookClient = new FacebookClient(
configuration.get[String]("pac4j.facebook.id"),
configuration.get[String]("pac4j.facebook.secret")
)
// Twitter OAuth configuration
val twitterClient = new TwitterClient(
configuration.get[String]("pac4j.twitter.id"),
configuration.get[String]("pac4j.twitter.secret")
)
// Google OpenID Connect configuration
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)
// Form authentication configuration
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()
}
}
Authentication Control in Controllers
// 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] {
// Authentication required action
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))
}
// Multiple clients support
def multiAuth: Action[AnyContent] =
Secure("FacebookClient,TwitterClient,GoogleOidcClient") { implicit request =>
val profiles = request.profiles
Ok(views.html.multiAuth(profiles))
}
// Admin authorization required
def admin: Action[AnyContent] =
Secure("FormClient", "admin") { implicit request =>
Ok(views.html.admin())
}
// Custom authorization
def customAuth: Action[AnyContent] =
Secure("FacebookClient", "custom") { implicit request =>
Ok("Custom authorized content")
}
// Profile information display
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))
}
}
Custom Authorizer
// 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) =>
// Custom authorization logic
profile.getAttribute("department") match {
case dept: String if dept == "Engineering" => true
case _ => false
}
case None => false
}
}
}
// Registration in SecurityModule.scala
@Provides
def provideConfig: Config = {
// ... clients configuration ...
val config = new Config(clients)
config.addAuthorizer("custom", new CustomAuthorizer())
config
}
Authentication in Async Actions
// 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
// Async processing
processUserData(userId).map { result =>
Ok(s"Processed: $result")
}
}.flatten
}
def processUserData(userId: String): Future[String] = {
// Async processing like database access
Future.successful(s"User data for $userId")
}
}
User Profile Management
// 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 Authentication
// 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"))
}
}
}
}