pac4j Scala

Authentication LibraryScalaPlay FrameworkOAuthSAMLOpenID ConnectAuthenticationAuthorization

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

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"))
      }
    }
  }
}