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:
- The controller is mixed with
org.pac4j.play.scala.Security[T]
that has the overloadedSecure
method to secure the controller routes. - The method
basicSecured
that uses theSecure
method. - 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:
- define a class
Filters
in the packageapp\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)
}
- load the filter in the
application.conf
play.http.filters = "filters.Filters"
- add the filters rules in the
application.conf
. Here we are securing the path/basicalternative
, using the instance ofDirectBasicAuthClient
defined in theSecurityModule
class.
pac4j.security {
rules = [
{"/basicalternative" = {
authorizers = "_authenticated_"
clients = "DirectBasicAuthClient"
}}
]
}
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:
- 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.