Basic Authentication

Here we are going to explain how to setup basic_auth. Please checkout the basic_auth branch.

Security Module

In the package app create a folder named module and place the class SecurityModule that extends AbstracModule. This class uses Google dependency injection and defines the client to be used which is DirectBasicAuthClient.

Both the clients uses as authenticator SimpleTestUsernamePasswordAuthenticator, that requires username equal to the password. However, in the next tutorials we are going to use external Authenticators (LDAP, and JWT).

Looking at the method provideConfig we can see how the client is bind to an instance of org.pac4j.core.config.Config.

package modules

import java.time.Duration

import com.google.inject.{AbstractModule, Provides}
import controllers.UnauthorizedHttpActionAdapter
import org.ldaptive.auth.{Authenticator, FormatDnResolver, PooledBindAuthenticationHandler, SearchDnResolver}
import org.ldaptive.pool._
import org.ldaptive.ssl.SslConfig
import org.ldaptive.{BindConnectionInitializer, ConnectionConfig, Credential, DefaultConnectionFactory}
import org.pac4j.core.client.Clients
import org.pac4j.core.config.Config
import org.pac4j.http.client.direct.{DirectBasicAuthClient, ParameterClient}
import org.pac4j.http.client.indirect.IndirectBasicAuthClient
import org.pac4j.jwt.config.signature.SecretSignatureConfiguration
import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator
import org.pac4j.ldap.profile.service.LdapProfileService
import org.pac4j.play.{CallbackController, LogoutController}
import org.pac4j.play.store.{PlayCacheSessionStore, PlaySessionStore}
import play.api.{Configuration, Environment}


class SecurityModule(env: Environment, conf: Configuration) extends AbstractModule {

  val baseUrl = conf.get[String]("baseUrl")

  override def configure(): Unit = {
    //The PlayCacheSessionStore is defined as the implementation for the session store: profiles will be saved in the Play Cache.
    bind(classOf[PlaySessionStore]).to(classOf[PlayCacheSessionStore])

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

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

  }

  private def getJwtAuthenticator = {
    val jwtAuthenticator = new JwtAuthenticator()
    jwtAuthenticator.addSignatureConfiguration(
      new SecretSignatureConfiguration(conf.get[String]("pac4j.jwt_secret"))
    )
    jwtAuthenticator
  }

  private def getLdapAuthenticator() = {
    val connectionConfig = new ConnectionConfig()
    connectionConfig.setConnectTimeout(
      Duration.ofMillis(conf.get[Long]("pac4j.ldap.conn_timeout")))
    connectionConfig.setResponseTimeout(
      Duration.ofMillis(conf.get[Long]("pac4j.ldap.resp_timeout")))
    connectionConfig.setLdapUrl(conf.get[String]("pac4j.ldap.url"))

    connectionConfig.setConnectionInitializer(
      new BindConnectionInitializer(
        conf.get[String]("pac4j.ldap.bind_dn"),
        new Credential(conf.get[String]("pac4j.ldap.bind_pwd"))
      )
    )

    connectionConfig.setUseSSL(true) //TODO Shall we keep SSL mandatory
    val sslConfig = new SslConfig()
    sslConfig.setTrustManagers() //TODO no more certificate validation, shall we keep it in this way?
    connectionConfig.setSslConfig(sslConfig)

    val connectionFactory = new DefaultConnectionFactory()
    connectionFactory.setConnectionConfig(connectionConfig)
    val poolConfig = new PoolConfig()
    poolConfig.setMinPoolSize(1)
    poolConfig.setMaxPoolSize(2)
    poolConfig.setValidateOnCheckIn(true)
    poolConfig.setValidateOnCheckOut(true)
    poolConfig.setValidatePeriodically(false)

    val searchValidator = new SearchValidator
    val pruneStrategy = new IdlePruneStrategy
    val connectionPool = new BlockingConnectionPool
    connectionPool.setPoolConfig(poolConfig)
    connectionPool.setBlockWaitTime(Duration.ofMillis(1000))
    connectionPool.setValidator(searchValidator)
    connectionPool.setPruneStrategy(pruneStrategy)
    connectionPool.setConnectionFactory(connectionFactory)
    connectionPool.initialize()
    val pooledConnectionFactory = new PooledConnectionFactory
    pooledConnectionFactory.setConnectionPool(connectionPool)

    val pooledBindHandler = new PooledBindAuthenticationHandler()
    pooledBindHandler.setConnectionFactory(pooledConnectionFactory)

    val dnResolver = new SearchDnResolver(connectionFactory)
    dnResolver.setBaseDn(conf.get[String]("pac4j.ldap.base_user_dn"))
    dnResolver.setUserFilter(
      s"(${conf.get[String]("pac4j.ldap.login_attribute")}={user})")

    val authenticator = new Authenticator()
    authenticator.setDnResolver(dnResolver)
    authenticator.setAuthenticationHandler(pooledBindHandler)

    val ldapProfileService = new LdapProfileService(connectionFactory, authenticator,
      conf.get[String]("pac4j.ldap.base_user_dn")
    )
    ldapProfileService.setAttributes("memberOf")
    ldapProfileService.setUsernameAttribute(conf.get[String]("pac4j.ldap.username_attribute"))

    ldapProfileService
  }



  //now we use ldap
  @Provides
  def directBasicAuthClient =
    new DirectBasicAuthClient(getLdapAuthenticator())


  @Provides
  def provideIndirectBasicAuthClient: IndirectBasicAuthClient =
    new IndirectBasicAuthClient(getLdapAuthenticator())

  @Provides
  def provideParameterClient: ParameterClient = {
    //authenticate using a simple not empty token
    val client = new ParameterClient("token", getJwtAuthenticator)
    client.setSupportGetRequest(true)
    client.setSupportPostRequest(false)
    client
  }

  @Provides
  def providesConfig(
    directBasicAuthClient: DirectBasicAuthClient,
    indirectBasicAuthClient: IndirectBasicAuthClient,
    parameterClient: ParameterClient,
  ): Config = {
    //  1. define the client
    val clients = new Clients(baseUrl + "/callback", directBasicAuthClient, indirectBasicAuthClient, parameterClient)

    //add the config for your clients
    val config = new Config(clients)
    config.setHttpActionAdapter(new UnauthorizedHttpActionAdapter())
    config
  }
}

Note that we add an instance of UnauthorizedHttpActionAdapter to return a page in case of authorization failure.

Finally, this module can be enabled by adding the following line in the file application.conf.


play.modules.enabled += "modules.SecurityModule"

Securing a Controller

Now it is time to try the security module by securing a route in a controller. This is done decorating a method with:


Secure("DirectBasicAuthClient") { profiles => //method logic and result }

A complete example is provide in the snippet below, where you can see how the method basicSecured is secured using the DirectBasicAuthClient. Note that:

  1. The controller is mixed with org.pac4j.play.scala.Security[T] that has the overloaded Secure method to secure the controller routes.
  2. The method basicSecured that uses the Secure method.
  3. The method basicAuthAlternative that uses Security Filters described below.
package controllers

import javax.inject.{Inject, _}
import org.pac4j.core.config.Config
import org.pac4j.core.context.Pac4jConstants
import org.pac4j.core.context.session.SessionStore
import org.pac4j.core.profile._
import org.pac4j.jwt.config.signature.SecretSignatureConfiguration
import org.pac4j.jwt.profile.JwtGenerator
import org.pac4j.play.PlayWebContext
import org.pac4j.play.scala._
import org.pac4j.play.store.PlaySessionStore
import play.api.mvc._
import org.pac4j.core.util.CommonHelper
import play.api.Configuration

import scala.collection.JavaConverters._

/**
 * This controller creates an `Action` to handle HTTP requests to the
 * application's home page.
 */
@Singleton
class HomeController @Inject() (
  val cc: ControllerComponents,
  val config: Config,
  val conf: Configuration,
  val playSessionStore: PlaySessionStore,
  val actionBuilder: DefaultActionBuilder
) extends AbstractController(cc) with Security[CommonProfile] {

  /**
   * Create an Action to render an HTML page.
   *
   * The configuration in the `routes` file means that this method
   * will be called when the application receives a `GET` request with
   * a path of `/`.
   */
  def index() = Action { implicit request: Request[AnyContent] =>
    val webContext = new PlayWebContext(request, playSessionStore)
    val sessionStore = webContext.getSessionStore.asInstanceOf[SessionStore[PlayWebContext]]
    val sessionId = sessionStore.getOrCreateSessionId(webContext)
    val csrfToken = sessionStore.get(webContext, Pac4jConstants.CSRF_TOKEN).asInstanceOf[String]
    Ok(views.html.index(getProfiles(request), sessionId, csrfToken))
  }

  def loginHttpForm = Secure("IndirectBasicAuthClient") { profiles =>
    actionBuilder { request =>
      Ok(views.html.protectedIndex(profiles))
    }
  }

  def secured() = Secure("DirectBasicAuthClient") { profiles =>
    actionBuilder { request =>
      Ok(views.html.protectedIndex(profiles))
    }
  }

  /**
    * check the application.conf to find the interceptor for the client to be used
    * @return
    */
  def securedFilters = actionBuilder { request =>
    val profiles = getProfiles(request)
    Ok(views.html.protectedIndex(profiles))
  }

  def jwtGenerate() = Secure("IndirectBasicAuthClient") { profiles =>
    actionBuilder { request =>
      val generator = new JwtGenerator[CommonProfile](
        new SecretSignatureConfiguration(conf.get[String]("pac4j.jwt_secret")))
      var token = ""
      if (CommonHelper.isNotEmpty(profiles.asJava)){
        token = generator.generate(profiles.asJava.get(0))
      }
      Ok(views.html.jwt.render(token))
    }
  }

  def securedJwt() = Secure("ParameterClient") {profiles =>
    actionBuilder { request =>
      Ok(views.html.protectedIndex(profiles))
    }
  }


  //this method can be moved to a common trait
  private def getProfiles(implicit request: RequestHeader): List[CommonProfile] = {
    val webContext = new PlayWebContext(request, playSessionStore)
    val profileManager = new ProfileManager[CommonProfile](webContext)
    val profiles = profileManager.getAll(true)
    asScalaBuffer(profiles).toList
  }
}

Using Security Filters

Alternatively, we can secure a set of routes via SecurityFilters. In order to do that we need to:

  1. define a class Filters in the package app\filters
package filters

import javax.inject.Inject

import org.pac4j.play.filters.SecurityFilter
import play.api.http.HttpFilters

class Filters @Inject()(securityFilter: SecurityFilter) extends HttpFilters {

  def filters = Seq(securityFilter)

}
  1. load the filter in the application.conf

play.http.filters = "filters.Filters"
  1. add the filters rules in the application.conf. Here we are securing the path /basicalternative, using the instance of DirectBasicAuthClient defined in the SecurityModule class.

pac4j.security { rules = [ {"/basicalternative" = { authorizers = "_authenticated_" clients = "DirectBasicAuthClient" }} ] }
Note

We can protect multiple routes by using wildcards and regular expression.


pac4j.security { rules = [ {"/filter/basicauth.*" = { authorizers = "_authenticated_" clients = "IndirectBasicAuthClient" }} {"/filter/.*" = { authorizers = "_authenticated_" }} ] }

This is what we need to setup basic authentication. Now we can start the application with sbt run and then do the following curl to login with basic auth using admin/admin (encoded) as credentials.


curl -X GET "http://localhost:9000/basicsecured" -H "accept: application/json" -H "Authorization: Basic YWRtaW46YWRtaW4="

Handling Authentication Errors

In order to make clear the errors in case of authentication errors we setup common helpers. More details can be found here

In the following we will see:

  1. how to use IndirectBasicAuthClient to perform the login using the browser
  2. how to use ParameterClient to perform the login via a param ?token
  3. how to perform the logout.