Require any role 🔗
Use this pattern for a broad requirement such as allowing administrators or support staff to read a report. Follow the example prerequisites.
Authenticate once, then guard the route body 🔗
Wrap the application's routes in one Pac4jSecurityHandler. Assign roles from trusted application state or verified identity-provider claims during authentication. The example authenticator assigns Admin, Support, both roles, or neither after validating the demonstration credentials.
Inside only the routes that need a role requirement, use the application helper:
case GET -> Path("reports") => authorizedAnyRole(Set(Role.Admin, Role.Support)) {
Response.withBody("Report")
}
authorizedAnyRole reads the roles on the authenticated pac4j profiles. One matching role is enough; additional roles do not prevent access. An empty allowed set grants nothing. Its action parameter is by-name, so denied requests never execute the protected body. The helper throws RejectedException on denial, mapped to 403 inside the security handler.
Keep this guard inside authenticated routes: pac4j handles missing or invalid credentials with 401 before dispatch. An authenticated request to an unknown route gets the ordinary 404; role guards run only after a route matches. No application-user lookup, context function, second security handler, or manual handler dispatch is needed.
For a requirement shared by the entire protected handler, you can instead register pac4j's RequireAnyRoleAuthorizer and configure its name through Pac4jSecurityConfig.authorizers. For an all-role requirement, use an explicitly named all-role guard or RequireAllRolesAuthorizer; keep any-role and all-role semantics distinct. Pac4j's empty role list has different semantics, so handle an empty allowed set explicitly when using its built-in authorizers.
Run and check access 🔗
scala run examples/scala-cli/authorization/02_roles.scala --server=false
curl -i -u admin:demo http://localhost:8181/reports # 200
curl -i -u support:demo http://localhost:8181/reports # 200
curl -i -u both:demo http://localhost:8181/reports # 200
curl -i -u reader:demo http://localhost:8181/reports # 403
curl -i http://localhost:8181/reports # 401
curl -i -u admin:demo http://localhost:8181/closed # 403: empty allowed set
curl -i http://localhost:8181/closed # 401
For a particular document, course, or tenant, add ownership, ABAC, or relationship checks in the service operation. A global role alone does not establish access to that resource.
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 02_roles.scala 02_roles.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.roles
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.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
import ba.sake.sharaf.exceptions.RejectedException
enum Role:
case Admin, Support
object RolesExample:
// 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("admin", "support", "both", "reader").contains(c.getUsername) && c.getPassword == "demo" =>
val profile = new CommonProfile()
profile.setId(c.getUsername)
val roles = c.getUsername match
case "admin" => Set(Role.Admin)
case "support" => Set(Role.Support)
case "both" => Set(Role.Admin, Role.Support)
case _ => Set.empty[Role]
roles.foreach(role => profile.addRole(role.toString))
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 security = Pac4jSecurityConfig(config(), clients = "basic", sessionStore = NoOpSessionStore())
Pac4jSecurityHandler(security, SharafHandler.exceptions(next, ExceptionMapper.json))
// Call inside an authenticated route; an empty set never grants access.
def authorizedAnyRole[A](roles: Set[Role])(action: => A): A =
val allowed = SecurityService.profiles.exists { profile =>
roles.exists(role => profile.getRoles.contains(role.toString))
}
if !allowed then throw RejectedException("Required role is missing")
action
def handler(): SharafHandler =
val routes = Routes {
case GET -> Path("reports") => authorizedAnyRole(Set(Role.Admin, Role.Support)) {
Response.withBody("Report")
}
case GET -> Path("closed") => authorizedAnyRole(Set.empty) {
Response.withBody("Must never be returned")
}
}
authenticated(SharafHandler.routes(routes))
def server(port: Int = 8181): UndertowSharafServer =
UndertowSharafServer("localhost", port, handler())
@main def runRoles(): Unit =
val app = server()
sys.addShutdownHook(app.stop())
app.start()
println("Server started at http://localhost:8181")