Other Clients

In the basic auth we used an authentication header to display a secured route, in the following we are going to 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.

Note that indirect clients are for UI authentication while direct for we service authentication

Indirect Basic Auth Client

It can be used to perform authentication using the included browser login form. The process flows as follow:

1) The originally requested URL is saved in session (by the “security filter”) 2) The user is redirected to the identity provider (by the “security filter”) 3) Authentication happens at the identity provider (or locally for the FormClient and the IndirectBasicAuthClient) 4) The user is redirected back to the callback endpoint/URL (“callback filter”) 5) The user is redirected to the originally requested URL (by the “callback filter”)

The parameter client can be used to perform login via a token (provided by a valid authorizer such as JWT)

Steps

Thus, in order to add it we need to setup an IndirectBasicAuthClient, add the need callbacks to handle login and logout, and update the client definition to handle the new login client.

Updates to the Security Module

  1. to update the configure adding a controller for callback and logout
  2. Add to the security module an instance of IndirectBasicAuthClient,
  3. update the Clients with the new client configuration plus the url to handle the callback,
  4. add a callback filter to handle the process
  5. add an ‘provideParameterClient’ to handle token access
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
  }
}

As you can note we are still using test authorizers like: 1. same username and password, or 2. non empty token

2. Update the controller

In the home controller we add a method definition for the new route (remember to add the route in the routes file).


GET /browserform controllers.HomeController.browserForm GET /paramsecured controllers.HomeController.paramSecured

add the two routes for the browser form and params


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

Other Clients

Pac4j supports other indirect clients like facebook, twitter, google app engine but we are not going to cover them here.