PAC4J : Security library for Play framework and Scala

# Introduction

At first, [Pac4](https://www.pac4j.org/) is developed as a Java library but can be use on Scala without any problem. It's also compatible with many different frameworks like Play, Spring ...

Here I will introduce how to implement this library in the Play Framework using Scala.

# 


# Dependencies 

I am using sbt for building :

```sbt:build.sbt
libraryDependencies += "org.pac4j" % "play-pac4j_2.12" % "10.0.2"
```

Next there are different modules for defining client and authenticator.
Client maybe direct(internal) or indirect(external)

But I will talk mainly about direct client.
Let's make a first Basic authentication and next a Bearer token authentication.
Login and password will be send as a Basic auth that will create a token and this token will be used for calling other api that required a Bearer authentication.

So we need 2 libraries :

```sbt:build.sbt
libraryDependencies += "org.pac4j" % pac4j-http" % "4.0.0"
libraryDependencies += "org.pac4j" % pac4j-jwt" % "4.0.0"
```

# Security Module

The security module define the different clients.
So at first let's enable a play modules :

```conf:application.conf
play.modules.enabled += "common.modules.SecurityModule"
```

And next define our module :

```scala:SecurityModule.scala

class SecurityModule(environment: Environment, configuration: Configuration) extends AbstractModule{
  val executionContext: ExecutionContext =
    new ExecutionContext {
      override def execute(runnable: Runnable): Unit = global.execute(runnable)
      override def reportFailure(cause: Throwable): Unit = global.reportFailure(cause)
    }
  override def configure(): Unit = {
    bind(classOf[PlaySessionStore]).to(classOf[PlayCacheSessionStore])
    bind(classOf[SecurityComponents]).to(classOf[DefaultSecurityComponents])
    bind(classOf[Pac4jScalaTemplateHelper[CommonProfile]])
  }
  // Use for email / Password authentication action
  @Provides
  def provideDirectBasicAuthClient: DirectBasicAuthClient = {
    val passwordAuthenticator = new PasswordAuthenticator(configuration)
    val basicClient = new DirectBasicAuthClient(passwordAuthenticator)
    basicClient
  }

  // Use for user token authentication
  @Provides
  def provideDirectBearerAuthClient: DirectBearerAuthClient = {
    val userAuthenticator = new UserAuthenticator(configuration)
    val bearerClient = new DirectBearerAuthClient(userAuthenticator)
    bearerClient
  }

  @Provides
  def provideConfig(provideDirectBearerAuthClient: DirectBearerAuthClient, provideDirectBasicAuthClient: DirectBasicAuthClient): Config = {
    val clients = new Clients("/callback", provideDirectBearerAuthClient, provideDirectBasicAuthClient, new AnonymousClient())
    val config = new Config(clients)
    config.addAuthorizer("role_authorizer", new RequireAnyRoleAuthorizer[CommonProfile]())
    config.setHttpActionAdapter(new PlayHttpActionAdapter())
    config
  }
}
```

In other term, this module read the configuration of the incoming url call,
if the header contains `Authentication: Basic ... `, the class PasswordAuthenticator will be called.
if the header contains `Authentication: Bearer ... `, the class UserAuthenticator will be called.

# Authenticator
The authenticator is here to validate credentials and next generate a user profile. 

## Basic Auth
Let's define our authenticator.

```scala:PasswordAuthenticator.scala
class PasswordAuthenticator @Inject() (configuration: Configuration) extends Authenticator[UsernamePasswordCredentials]{
  @Override
  override def validate(credentials: UsernamePasswordCredentials, context: WebContext): Unit = {
    // Validation
    if (credentials == null) throw new CredentialsException("No credential")
    val username = credentials.getUsername
    val password = credentials.getPassword
    if (CommonHelper.isBlank(username)) throw new CredentialsException("Username cannot be blank")
    if (CommonHelper.isBlank(password)) throw new CredentialsException("Password cannot be blank")

    // Login Password verification - you can implement a DB verification
    if (username != "test" && password != "test") throw new CredentialsException("User not found")

    // User is verified, we create a profile
    val profile = new CommonProfile
    profile.setId(1)
    profile.setRoles(Set(Role.USER.role).asJava)
    profile.addAttribute("TEST",123)  // We can add any additional attributes
    credentials.setUserProfile(profile)
  }
}
```
Here the authenticator just decode the Basic token and check username and password before creating a user profile.
The user profile can be read from the controller.

Let's define our urls:

```conf:routes.conf
POST        /login                      controllers.AuthController.login      ## Basic Auth required
GET         /get_profile                controllers.AuthController.profile    ## Bearer Auth required
```

Next define our controller:

```scala:AuthController.scala
class AuthController @Inject()(
                                val controllerComponents: SecurityComponents,
                                configuration: Configuration,
                                implicit val executionContext: ExecutionContext,
                                implicit val pac4jTemplateHelper: Pac4jScalaTemplateHelper[CommonProfile]
                              )  extends Security[CommonProfile] {

  // Secure means the login function should use the security module.
  // DirectBasicAuthClient is the only client method allowed
  // Authorizers define witch user are allowed to access it. Here all the profile are allowed
  def login() = Secure("DirectBasicAuthClient", authorizers = "role_authorizer") { request =>
    // User profile can be accessed with request.profiles.head (CommonProfile)
    // Now we create a JWT token with an expire time
    val signatureToken="I want one token please, I am a normal user and I am a good guy"
    val tokenExpireDays=1
    val (token, expireAt) = generateToken(request.profiles, signatureToken, tokenExpireDays)

    Ok(Json.obj(
      "token" -> token,
      "expireAt" -> expireAt
    ))
  }

  // Generate a JWT Token based on user profile
  def generateToken(profiles: List[CommonProfile], signature: String, expire: Int): (String, ZonedDateTime) = {
    val generator = new JwtGenerator[CommonProfile](new SecretSignatureConfiguration(signature))
    val expireAt = ZonedDateTime.now().plusDays(expire)
    generator.setExpirationTime(Date.from(expireAt.toInstant))
    var token: String = ""
    if (CommonHelper.isNotEmpty(profiles.asJava)) {
      try {
        token = generator.generate(profiles.asJava.get(0))
      } catch {
        case e:Throwable => {
          throw RuntimeException(s"Error binding timezone.(columnLabel: }")
        }
      }
    }
    (token,expireAt)
  }

}
```

`login()` can only be called from a Basic Authentication and with an existing user.

## Bearer Auth
Let's define our authenticator.

```scala:UserAuthenticator.scala
class UserAuthenticator @Inject()(configuration: Configuration) extends JwtAuthenticator {
  
  val signature="I want one token please, I am a normal user and I am a good guy"

  @Override
  override def validate(credentials: TokenCredentials, context: WebContext): Unit = {
      super.addSignatureConfiguration(new SecretSignatureConfiguration(signature))
      super.validate(credentials, context)
      val commonProfile = credentials.getUserProfile()
      if (commonProfile != null) {
            commonProfile.setClientName("Toto") // Add some attributes...
            commonProfile.addAttribute("new attribute", "test") // Add some attributes...
            credentials.setUserProfile(commonProfile)
      }
   }
}
```
When calling super.validate() the signature is used to decode and validate the token, if the token is not valid a credential exception is throw. An expire token will also return a credential exception.
Once the token is read, a commonProfile is created and can be read in the Controller.
Let's define an additional method to our controller.

```scala:AuthController.scala
  // Secure means the login function should use the security module.
  // DirectBearerAuthClient is the only client method allowed
  // Authorizers define witch user are allowed to access it. Here all the profile are allowed
  def profile() = Secure("DirectBearerAuthClient", authorizers = "role_authorizer") { request =>
    ...
    // Now we can display our user profile
    Ok(Json.obj(
      "id" -> request.profiles.head.getId,
      "name" -> request.profiles.head.getClientName,
      "other_attribute" -> request.profiles.head.getAttribute("new attribute")
    ))
  }
```

# Replacing easily an auth client
Let's say we want to transmit the token with a cookie and not a bearer auth.
We will add :

```scala:SecurityModule.scala
  // Use for cookie token authentication
  @Provides
  def provideCookieClient: CookieClient = {
    val userAuthenticator = new UserAuthenticator(configuration)
    val cookieClient = new CookieClient("project-token",userAuthenticator)
    cookieClient
  }

   @Provides
  def provideConfig(provideDirectBearerAuthClient: DirectBearerAuthClient, provideDirectBasicAuthClient: DirectBasicAuthClient, provideCookieClient: CookieClient): Config = {
    val clients = new Clients("/callback", provideDirectBearerAuthClient, provideDirectBasicAuthClient, provideCookieClient, new AnonymousClient())
    val config = new Config(clients)
    config.addAuthorizer("role_authorizer", new RequireAnyRoleAuthorizer[CommonProfile]())
    config.setHttpActionAdapter(new PlayHttpActionAdapter())
    config
  }
```
If a cookie named "project-token" is present it will trigger the UserAuthenticator.
Next modify our controller

```scala:AuthController.scala
def profile() = Secure("CookieClient", authorizers = "role_authorizer") {
...
```

If you want to set the cookie :

```scala:AuthController.scala
  def login() = Secure("DirectBasicAuthClient", authorizers = "role_authorizer") { request =>
...
    Ok(json).withCookies(Cookie(
            name = "project-token",
            value = token,
            maxAge = expireAt,
            secure = request.connection.secure
          ))
}
```

We can also provide 2 clients:

```scala:AuthController.scala
def profile() = Secure("DirectBearerAuthClient,CookieClient", authorizers = "role_authorizer") {
...
```

# Indirect Client
We can also use indirect client like OpenID Connect

```sbt:build.sbt
libraryDependencies += "org.pac4j" % "pac4j-oidc" % "4.0.0" exclude("commons-io", "commons-io")
```

```scala:SecurityModule.scala
  @Provides
  def provideOidcClient: OidcClient[OidcProfile, OidcConfiguration] = {
    val oidcConfiguration = new OidcConfiguration()
    oidcConfiguration.setClientId(client_id)
    oidcConfiguration.setSecret(client_secret)
    oidcConfiguration.setDiscoveryURI(discoveryURI)
    oidcConfiguration.setExpireSessionWithToken(true)
    //oidcConfiguration.addCustomParam("prompt", "consent")

    val oidcClient = new OidcClient[OidcProfile, OidcConfiguration](oidcConfiguration)
    oidcClient.addAuthorizationGenerator((context: WebContext, profile: OidcProfile) => {
      profile.addRole("ROLE_DEV")
      profile
    })
    oidcClient
  }

  @Provides
  def provideOidcHeaderClient: HeaderClient = {
    val oidcConfiguration = new OidcConfiguration()
    oidcConfiguration.setClientId(client_id)
    oidcConfiguration.setSecret(client_secret)
    oidcConfiguration.setDiscoveryURI(discoveryURI)
    //oidcConfiguration.addCustomParam("prompt", "consent")

    val authenticator = new UserInfoOidcAuthenticator(oidcConfiguration)
    val headerClient = new HeaderClient("Authorization", "Bearer ", authenticator)
    headerClient
  }

  @Provides
  def provideConfig(oidcHeaderClient: HeaderClient, oidcClient: OidcClient[OidcProfile, OidcConfiguration]): Config = {
    val clients = new Clients(base_url + "/callback", oidcHeaderClient, oidcClient, new AnonymousClient())

    val config = new Config(clients)
    config.addAuthorizer("role_authorizer", new RequireAnyRoleAuthorizer[Nothing]("ROLE_DEV"))
    config.setHttpActionAdapter(new PlayHttpActionAdapter())
    config
  }

```

Next we can define our controller

```scala:AuthController.scala
def index() = Secure("OidcClient", authorizers = "role_authorizer")) {
...

def profile() = Secure("HeaderClient") {
...
```
Here the profile can be access with `request.profiles.head` and the profile is store in an `OidcProfile` Object.

When you try to access the url without Bearer token you will be redirected to the external login url page `discoveryURI` 

# Conclusion
pac4j is a really an useful library to manage easily authentication, role and user profile on a project.
It can be use in many different framework and language.

:+1: