Security

Web application security restricts access to resources within the web application, or imposes security requirements on the transport with which these resources are delivered to clients.

The processing of the request may be:

  • forbidden; the request is immediately responded with a 403 status code.

  • allowed; the request processing is allowed to continue to the application, which produces a response.

  • challenged; the request may be allowed, but the credentials are missing (or wrong), so the request is responded either with a 401 status code, or with a response indicating that credentials are required (for example, a login page or a redirect to a login page.

Jetty implements web application security with 3 components:

  • A subclass of org.eclipse.jetty.security.SecurityHandler, see this section for the implementations available out-of-the-box.

  • An implementation of org.eclipse.jetty.security.Authenticator, see this section for the implementations available out-of-the-box.

  • An implementation of org.eclipse.jetty.security.LoginService, see this section for the implementations available out-of-the-box.

These components interact in this way:

Diagram
  1. The SecurityHandler subclass returns a Constraint based on the subclass-specific logic, for example based on the request path, the request method, etc.

  2. The Constraint is used to check if the request is trivially allowed, or trivially forbidden, or whether it requires a secure transport, and if so immediately handled by SecurityHandler with no further actions. Otherwise, the Constraint declares what requirements about authorization, transport and roles are necessary for the request to be allowed.

  3. The SecurityHandler calls Authenticator.validateRequest(...) that performs implementation-specific logic to retrieve the authentication credentials, for example from HTTP request headers such as Authorization.

  4. The Authenticator calls LoginService.login(...) to verify the credentials and, if the verification is successful, obtain information about the roles associated with these credentials.

  5. The Authenticator builds an AuthenticationState with the results of the call to LoginService.login(...), and returns it to the SecurityHandler.

  6. The SecurityHandler, based on the received AuthenticationState, either allows the request to be processed by its child Handler, or sends an appropriate response to the client, for example a 401 challenge response or a 403 forbidden response.

Configuring Jetty Core Security

This section is about configuring security for Jetty Core web applications. To configure security for Jakarta EE web applications, see this section.

SecurityHandler is typically configured as a child of the ContextHandler that represents the web application.

A simple example uses SecurityHandler.PathMapped along with BasicAuthenticator and HashLoginService:

class AppHandler extends Handler.Abstract
{
    @Override
    public boolean handle(Request request, Response response, Callback callback)
    {
        // Retrieve the authenticated user for this request.
        Principal principal = Request.getAuthenticationState(request).getUserPrincipal();
        System.getLogger("app").log(INFO, "Current user is: {0}", principal);

        callback.succeeded();
        return true;
    }
}

Server server = new Server();

// The ContextHandler for the application.
ContextHandler contextHandler = new ContextHandler("/app");

// HashLoginService maps users, passwords and roles
// from the realm.properties file in the class-path.
HashLoginService loginService = new HashLoginService();
loginService.setConfig(ResourceFactory.of(contextHandler).newClassLoaderResource("realm.properties"));

// Use Basic authentication, which requires a secure transport.
BasicAuthenticator authenticator = new BasicAuthenticator();
authenticator.setLoginService(loginService);

// The SecurityHandler.PathMapped maps URI paths to constraints.
SecurityHandler.PathMapped securityHandler = new SecurityHandler.PathMapped();
securityHandler.setAuthenticator(authenticator);
securityHandler.setLoginService(loginService);

// Configure constraints.
// Require that all requests use a secure transport.
securityHandler.put("/*", Constraint.SECURE_TRANSPORT);
// URI paths that start with /admin/ can only be accessed by users with the "admin" role.
securityHandler.put("/admin/*", Constraint.from("admin"));

// Link the Handlers.
server.setHandler(contextHandler);
contextHandler.setHandler(securityHandler);
securityHandler.setHandler(new AppHandler());

server.start();

The Handler tree structure looks like the following:

Server
└── ContextHandler /app
    └── SecurityHandler
        └── AppHandler

The realm.properties file used by HashLoginService is the following:

realm.properties
# Format: <user>:<password>,<roles>
carol:password
david:password,admin

The behavior of the example above is the following:

  • For any request, all the request path matches are collected and processed in order from the least significant to the most significant, combining their constraint.

  • A non-secure request using the http scheme is replied with a 403 response, since secure transport is required.

  • A request for /app/foo matches the constraint mapped to /*, and is replied with a 200 response; AppHandler would see a null request principal.

  • A request for /app/admin/bar without Authorization header matches both the constraints mapped to /* and /admin/*, and is replied with a 401 response, since authentication is required.

  • A request for /app/admin/bar with an Authorization header containing invalid or unknown credentials is replied with a 401 response, since authentication is required.

  • A request for /app/admin/bar with an Authorization header containing valid credentials with a role that is not admin is replied with a 401 response, since authentication is required.

  • A request for /app/admin/bar with an Authorization header containing valid credentials for user david, that has admin role, is replied with a 200 response, and AppHandler would see a request principal for user david.

It is good practice to have a default constraint for all paths (that is, for path /*), even if it is Constraint.ALLOWED. In this way it is clear what is the intended behavior for paths that do not match more specific constraint configurations.

The more complex example below uses SecurityHandler.PathMethodMapped to allow users with the read role to read resources, and users with the write role to write resources:

class AppHandler extends Handler.Abstract
{
    @Override
    public boolean handle(Request request, Response response, Callback callback)
    {
        // Retrieve the authenticated user for this request.
        Principal principal = Request.getAuthenticationState(request).getUserPrincipal();
        System.getLogger("app").log(INFO, "Current user is: {0}", principal);

        callback.succeeded();
        return true;
    }
}

Server server = new Server();

// The ContextHandler for the application.
ContextHandler contextHandler = new ContextHandler("/app");

// HashLoginService maps users, passwords and roles
// from the realm.properties file in the class-path.
HashLoginService loginService = new HashLoginService();
loginService.setConfig(ResourceFactory.of(contextHandler).newClassLoaderResource("realm.properties"));

// Use Basic authentication, which requires a secure transport.
BasicAuthenticator authenticator = new BasicAuthenticator();
authenticator.setLoginService(loginService);

// The SecurityHandler.PathMapped maps URI paths to constraints.
SecurityHandler.PathMethodMapped securityHandler = new SecurityHandler.PathMethodMapped();
securityHandler.setAuthenticator(authenticator);
securityHandler.setLoginService(loginService);

// Configure constraints.
// Unless otherwise specified, access to resources is forbidden and requires secure transport.
securityHandler.put("/*", "*", Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT));
// GET /data/* is allowed only to users with the "read" role.
securityHandler.put("/data/*", "GET", Constraint.from("read"));
// PUT /data/* is allowed only to users with the "write" role.
securityHandler.put("/data/*", "PUT", Constraint.from("write"));

// Link the Handlers.
server.setHandler(contextHandler);
contextHandler.setHandler(securityHandler);
securityHandler.setHandler(new AppHandler());

server.start();
realm.properties
# Format: <user>:<password>,<roles>
bob:password,read
alice:password,read,write

In the example above, access to any resource is by default forbidden, and requires secure transport.

However, access to /data/* resources using the HTTP method GET is granted but only to users with read role. Both bob and alice are granted access to these resources, but only if they use the GET method.

Similarly, access to /data/* resources using the HTTP method PUT is granted but only to users with write role. Only alice is granted access to these resources, but only if she uses the PUT method.

It is good practice to have a default constraint for all paths (that is, for path /*), and for all HTTP methods (that is, for method *), even if it is Constraint.ALLOWED. In this way it is clear what is the intended behavior for paths and HTTP methods that do not match more specific constraint configurations.

The behavior of SecurityHandler implementations for the Jetty Core API is to combine all the constraints that match. This is different from the behavior of the SecurityHandler implementations for the Jakarta EE APIs, that only use the constraint of the most specific match.

Configuring Jakarta EE Security

This section is about configuring security for Jakarta EE web applications. To configure security for Jetty Core web applications, see this section.

To configure Jakarta EE web application security you must use the SecurityHandler subclass org.eclipse.jetty.ee11.servlet.security.ConstraintSecurityHandler.

Below you can find a simple example that sets up a ConstraintSecurityHandler to secure your Jakarta EE web application:

Server server = new Server();

WebAppContext webApp = new WebAppContext();
webApp.setContextPath("/app");
webApp.setWar("/path/to/app.war");

// HashLoginService maps users, passwords and roles
// from the realm.properties file in the server class-path.
HashLoginService loginService = new HashLoginService();
loginService.setConfig(ResourceFactory.of(webApp).newClassLoaderResource("realm.properties"));

// Use Basic authentication, which requires a secure transport.
BasicAuthenticator authenticator = new BasicAuthenticator();
authenticator.setLoginService(loginService);

ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setAuthenticator(authenticator);
securityHandler.setLoginService(loginService);

// Configure constraints.
ConstraintMapping constraintMapping = new ConstraintMapping();
constraintMapping.setPathSpec("/*");
constraintMapping.setConstraint(Constraint.SECURE_TRANSPORT);
securityHandler.addConstraintMapping(constraintMapping);
constraintMapping = new ConstraintMapping();
constraintMapping.setPathSpec("/admin/*");
constraintMapping.setConstraint(Constraint.from("admin"));
securityHandler.addConstraintMapping(constraintMapping);

// Link the Handlers.
server.setHandler(webApp);
// Note the specific call to setSecurityHandler().
webApp.setSecurityHandler(securityHandler);

server.start();

The Handler tree structure looks like the following:

Server
└── WebAppContext /app
    └── ConstraintSecurityHandler
        └── ServletHandler
            └── AppServlet (defined in app.war)

The realm.properties file used by HashLoginService is the following:

realm.properties
# Format: <user>:<password>,<roles>
carol:password
david:password,admin

Note how the constraints are configured using org.eclipse.jetty.ee11.servlet.security.ConstraintMapping, that allows to configure the path, the HTTP method and the Constraint.

The more complex example below uses ConstraintSecurityHandler to allow users with the read role to read resources, and users with the write role to write resources:

Server server = new Server();

WebAppContext webApp = new WebAppContext();
webApp.setContextPath("/app");
webApp.setWar("/path/to/app.war");

// HashLoginService maps users, passwords and roles
// from the realm.properties file in the server class-path.
HashLoginService loginService = new HashLoginService();
loginService.setConfig(ResourceFactory.of(webApp).newClassLoaderResource("realm.properties"));

// Use Basic authentication, which requires a secure transport.
BasicAuthenticator authenticator = new BasicAuthenticator();
authenticator.setLoginService(loginService);

ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setAuthenticator(authenticator);
securityHandler.setLoginService(loginService);

// Configure constraints.
// Forbid access for uncovered HTTP methods.
securityHandler.setDenyUncoveredHttpMethods(true);
// No HTTP method specified, therefore applies to all methods.
ConstraintMapping constraintMapping = new ConstraintMapping();
constraintMapping.setPathSpec("/*");
constraintMapping.setConstraint(Constraint.combine(Constraint.FORBIDDEN, Constraint.SECURE_TRANSPORT));
securityHandler.addConstraintMapping(constraintMapping);
// GET /data/* is allowed only to users with the "read" role.
constraintMapping = new ConstraintMapping();
constraintMapping.setPathSpec("/data/*");
constraintMapping.setMethod("GET");
constraintMapping.setConstraint(Constraint.combine(Constraint.SECURE_TRANSPORT, Constraint.from("read")));
// PUT /data/* is allowed only to users with the "write" role.
constraintMapping = new ConstraintMapping();
constraintMapping.setPathSpec("/data/*");
constraintMapping.setMethod("PUT");
constraintMapping.setConstraint(Constraint.combine(Constraint.SECURE_TRANSPORT, Constraint.from("write")));

// Link the Handlers.
server.setHandler(webApp);
// Note the specific call to setSecurityHandler().
webApp.setSecurityHandler(securityHandler);

server.start();
realm.properties
# Format: <user>:<password>,<roles>
bob:password,read
alice:password,read,write

In the example above, access to any resource is by default forbidden, and requires secure transport.

However, access to /data/* resources using the HTTP method GET is granted but only to users with read role. Both bob and alice are granted access to these resources, but only if they use the GET method.

Similarly, access to /data/* resources using the HTTP method PUT is granted but only to users with write role. Only alice is granted access to these resources, but only if she uses the PUT method.

Note also that ConstraintSecurityHandler.denyUncoveredHttpMethods=true to make sure that requests that match /data/* but are neither GET nor PUT are denied.

The behavior of SecurityHandler implementations for the Jakarta EE API is to only use the constraint of the most specific match. This is different from the behavior of the SecurityHandler implementations for the Jetty Core APIs, that combine all the constraints that match.

SecurityHandler Implementations

Jetty provides the following SecurityHandler implementations:

  • SecurityHandler.PathMapped, that allows you to configure constraints based on the request path.

  • SecurityHandler.PathMethodMapped, that allows you to configure constraints based on the request path and the request HTTP method.

  • ConstraintSecurityHandler, that implements the Jakarta EE web application security constraints.

Authenticator Implementations

Jetty provides the following Authenticator implementations:

LoginService Implementations

Jetty provides the following LoginService implementations:

  • HashLoginService, that verifies credentials retrieved from a *.properties file.

  • JDBCLoginService, that verifies credentials retrieved from a RDBMS using the JDBC APIs.

  • DataSourceLoginService, that verifies credentials retrieved from a RDBMS using the DataSource APIs.

  • JAASLoginService, that verifies credentials retrieved using the JAAS APIs.

  • AnyUserLoginService, that does not verify credentials, but can delegate to another LoginService to provide roles for authenticated users.

Some Authenticator implementation require a specific LoginService, so the following implementation are also available:

  • SPNEGOLoginService, used in conjunction with SPNEGOAuthenticator.

  • OpenIdLoginService, used in conjunction with OpenIdAuthenticator.