Other Clients
In the basic auth we used an authentication header to display a secured route, in the following we are going to see:
- how to use
IndirectBasicAuthClient
to perform the login using the browser - how to use
ParameterClient
to perform the login via a param?token
- 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
- to update the
configure
adding a controller for callback and logout - Add to the security module an instance of
IndirectBasicAuthClient
, - update the
Clients
with the new client configuration plus the url to handle the callback, - add a
callback
filter to handle the process - 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.