Authorizers

In this page we are going to replace dummy username/password and non empty token authorizers with a valid LDAP and JWT authorizers. In order to test the application we need to run ldap test serverhttps://github.com/rroemhild/docker-test-openldap) as local service via docker

docker run --privileged -d -p 389:389 -p 636:636 --name ldap rroemhild/test-openldap

LDAP

Add the configuration for ldap in the SecurityModule:

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

In particular, check the getLdapAuthenticator method and the directBasicAuthClient that now uses ldap as provider for authentication.

And then add the new routes in the HomeController

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