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
403status 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
401status 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:
-
The
SecurityHandlersubclass returns aConstraintbased on the subclass-specific logic, for example based on the request path, the request method, etc. -
The
Constraintis used to check if the request is trivially allowed, or trivially forbidden, or whether it requires a secure transport, and if so immediately handled bySecurityHandlerwith no further actions. Otherwise, theConstraintdeclares what requirements about authorization, transport and roles are necessary for the request to be allowed. -
The
SecurityHandlercallsAuthenticator.validateRequest(...)that performs implementation-specific logic to retrieve the authentication credentials, for example from HTTP request headers such asAuthorization. -
The
AuthenticatorcallsLoginService.login(...)to verify the credentials and, if the verification is successful, obtain information about the roles associated with these credentials. -
The
Authenticatorbuilds anAuthenticationStatewith the results of the call toLoginService.login(...), and returns it to theSecurityHandler. -
The
SecurityHandler, based on the receivedAuthenticationState, either allows the request to be processed by its childHandler, or sends an appropriate response to the client, for example a401challenge response or a403forbidden 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:
# 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
httpscheme is replied with a403response, since secure transport is required. -
A request for
/app/foomatches the constraint mapped to/*, and is replied with a200response;AppHandlerwould see anullrequest principal. -
A request for
/app/admin/barwithoutAuthorizationheader matches both the constraints mapped to/*and/admin/*, and is replied with a401response, since authentication is required. -
A request for
/app/admin/barwith anAuthorizationheader containing invalid or unknown credentials is replied with a401response, since authentication is required. -
A request for
/app/admin/barwith anAuthorizationheader containing valid credentials with a role that is notadminis replied with a401response, since authentication is required. -
A request for
/app/admin/barwith anAuthorizationheader containing valid credentials for userdavid, that hasadminrole, is replied with a200response, andAppHandlerwould see a request principal for userdavid.
|
It is good practice to have a default constraint for all paths (that is, for path |
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();
# 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 |
|
The behavior of |
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:
# 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();
# 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
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:
-
BasicAuthenticator, that implements Basic authentication. -
DigestAuthentication, that implements Digest authentication. -
FormAuthenticator, that implements HTML form authentication using thej_security_checkmechanism. -
SslClientCertAuthenticator, that implements authentication based on client certificates. -
SPNEGOAuthenticator, that implements SPNEGO authentication. -
EthereumAuthenticator, that implements Sign-In with Ethereum authentication (see also this dedicated section). -
OpenIdAuthenticator, that implements OpenID Connect Core 1.0 authentication (see also this dedicated section). -
MultiAuthenticator, that supports multiple authentication mechanisms for the same web application. -
JaspiAuthenticator, for the Jakarta Authentication API.
LoginService Implementations
Jetty provides the following LoginService implementations:
-
HashLoginService, that verifies credentials retrieved from a*.propertiesfile. -
JDBCLoginService, that verifies credentials retrieved from a RDBMS using the JDBC APIs. -
DataSourceLoginService, that verifies credentials retrieved from a RDBMS using theDataSourceAPIs. -
JAASLoginService, that verifies credentials retrieved using the JAAS APIs. -
AnyUserLoginService, that does not verify credentials, but can delegate to anotherLoginServiceto provide roles for authenticated users.
Some Authenticator implementation require a specific LoginService, so the following implementation are also available:
-
SPNEGOLoginService, used in conjunction withSPNEGOAuthenticator. -
OpenIdLoginService, used in conjunction withOpenIdAuthenticator.