Require authentication 🔗

Use this pattern when every authenticated caller may access an endpoint and its body does not need application-user data. Follow the example prerequisites.

Protect the handler 🔗

Define an application authenticated(next: SharafHandler) helper that constructs a single Pac4jSecurityHandler around all routes. Register a pac4j PathMatcher to exempt the exact landing path / and the public branch /public. Keep pac4j's security-header matchers in the matcher list. Routes remain in one normal Routes definition; there is no manual handler dispatch or duplicated path matching.

The example uses excludePath("/") for an exact path and excludeBranch("/public") for /public and its descendants. /public/help is public, while /publicity still requires authentication. Path exemptions apply to every HTTP method; only the explicitly defined route/method combinations return content.

If your application intentionally protects only a prefix, includePath("/api/") applies security to paths starting with /api/; it does not cover /api itself, and other paths bypass security. Use explicit public exemptions when the application should require authentication by default.

The /members route returns fixed content. It never calls SecurityService.currentUser, resolves a domain user, or introduces a user context parameter. Authentication establishes access without requiring identity in the route body.

Run and check access 🔗

scala run examples/scala-cli/authorization/01_authenticated.scala --server=false

In another terminal:

curl -i http://localhost:8181/                       # 200 Public
curl -i http://localhost:8181/public/help            # 200 Public help
curl -i http://localhost:8181/publicity              # 401: neighboring prefix
curl -i http://localhost:8181/members                # 401; WWW-Authenticate: Basic ...
curl -i -u alice:demo http://localhost:8181/members  # 200 Members only
curl -i -u alice:wrong http://localhost:8181/members # 401

bob:demo is also valid. No session cookie is created. An authenticated request to an unknown path receives 404; an anonymous request to it still requires authentication.

If a route needs to query user-owned data, continue with application identity and ownership. For a broad role requirement, use authorizedAnyRole.

Run this example's integration tests 🔗

From the example folder, run the application source, its adjacent tests, and the shared HTTP fixture together:

cd examples/scala-cli/authorization
scala test 01_authenticated.scala 01_authenticated.test.scala HttpExampleSuite.test.scala --server=false

This starts and stops the real server on a free port. No other examples or manually started servers are needed.

Complete runnable example 🔗

Source. The listing is synchronized from the tested source.

//> using scala 3.9.0
//> using dep ba.sake::sharaf-undertow:0.21.0
//> using dep ba.sake::sharaf-pac4j:0.21.0
//> using dep org.pac4j:pac4j-http:6.5.8
//> using options -Yretain-trees

package authorization.authentication

import java.util.Optional
import org.pac4j.core.client.Clients
import org.pac4j.core.config.Config
import org.pac4j.core.context.CallContext
import org.pac4j.core.credentials.{Credentials, UsernamePasswordCredentials}
import org.pac4j.core.credentials.authenticator.Authenticator
import org.pac4j.core.profile.CommonProfile
import org.pac4j.core.matching.matcher.{DefaultMatchers, PathMatcher}
import org.pac4j.http.client.direct.DirectBasicAuthClient
import ba.sake.sharaf.*
import ba.sake.sharaf.pac4j.*
import ba.sake.sharaf.session.NoOpSessionStore
import ba.sake.sharaf.undertow.UndertowSharafServer

object AuthenticationExample:
  // Development credentials only: each listed username uses the password "demo".
  private def config(): Config =
    val authenticator = new Authenticator:
      override def validate(ctx: CallContext, credentials: Credentials): Optional[Credentials] =
        credentials match
          case c: UsernamePasswordCredentials
              if Set("alice", "bob").contains(c.getUsername) && c.getPassword == "demo" =>
            val profile = new CommonProfile()
            profile.setId(c.getUsername)
            c.setUserProfile(profile)
            Optional.of(c)
          case _ => Optional.empty()
    val client = new DirectBasicAuthClient(authenticator)
    client.setName("basic")
    new Config(new Clients(client))

  def authenticated(next: SharafHandler): SharafHandler =
    val pac4j = config()
    val publicPaths = new PathMatcher()
    publicPaths.excludePath("/")
    publicPaths.excludeBranch("/public")
    pac4j.addMatcher("publicPaths", publicPaths)
    val security = Pac4jSecurityConfig(
      pac4j, clients = "basic", matchers = s"${DefaultMatchers.SECURITYHEADERS},publicPaths",
      sessionStore = NoOpSessionStore()
    )
    Pac4jSecurityHandler(security, next)

  def handler(): SharafHandler =
    val routes = Routes {
      case GET -> Path()                 => Response.withBody("Public")
      case GET -> Path("public", "help") => Response.withBody("Public help")
      case GET -> Path("members")        => Response.withBody("Members only")
    }
    authenticated(SharafHandler.routes(routes))

  def server(port: Int = 8181): UndertowSharafServer =
    UndertowSharafServer("localhost", port, handler())

  @main def runAuthentication(): Unit =
    val app = server()
    sys.addShutdownHook(app.stop())
    app.start()
    println("Server started at http://localhost:8181")