JAX-RS 2: server security filter
JAX-RS 2 added to the specification a lot of missing components in 1.0 version like request/response filters for the server and client.
These ones are really helpful for all transversal concerns like the most famous one: the security.
Let's see how to setup Basic security (the "authorization" scheme name) with this new API.
Before digging in JAX-RS API let's keep in mind you can still use a plain Servlet filter to secure JAX-RS endpoints....so why using a specific component? To get more specific information on the underlying endpoint without playing with a lot of configuration or path detection.
Here is an example:
- I have 2 endpoints /login and /loginformation
- /loginformation should be executed in a logged context and willr eturn the user data (name, displayname etc...)
- /login will obviously log the user in so is executed in an anonymous context
A filter will need this kind of logic (pseudo code):
doFilter(req, resp, chain) {
if (!isLogin(req)) {
doLogin();
}
chain.doFiltre(req, resp);
}
This code is not a big deal until you implement isLogin():
isLogin(request) {
String path = request.getRequestURI().substring(request.getContextPath().length());
return "/login".equals(path);
}
In plain words: you will quickly need to do some advanced path matching to exclude the anonymous endpoints from the secured ones.
Now assume you want to go one step further and want to secure all endpoints not having @HeaderParam("Authorization") or @Anonymous cause by convention your anonymous endpoints will have this header...then you are stucked cause Servlet layer does have it.
That's where the JAX-RS layer is very useful and makes this kind of logic really smooth.
To do that we'll use a DynamicFeature. A quick reminder is a Feature is a registration hook in JAX-RS runtime. The dynamic part means you will get endpoint metadata at registration part. Here is the signature:
public interface DynamicFeature {
void configure(ResourceInfo resourceInfo, FeatureContext context);
}
The ResourceInfo will give you access to the endpoint Method and Class. In most containers you can use injections in your instance and as any provider you can mark it @Provider to automatically add it to your runtime:
@Provider
@Dependent
public class BasicFeature implements DynamicFeature {
@Inject
private MyService service;
@Override
public void configure(final ResourceInfo resourceInfo, final FeatureContext context) {
if (LoginEndpoint.class.isAssignableFrom(resourceInfo.getResourceClass()) ||
resourceInfo.getResourceMethod().isAnnotationPresent(Anonymous.class)) {
return;
}
// ...
}
}
In this snippet you can see that the filtering logic is easier cause based on the actual java code which is the JAX-RS natural language.
Now how to handle basic login? Login should happen before the execution to execute the method in a logged context so we'll use a ContainerRequestFilter responsible to read the Authorization header and log the user in. Since we can get injection in our feature the filter can inherit from it (either being a nested class or by its constructor). This means you can reuse your business logic.
Here is a potential BasicFilter implementation:
public class BasicFilter implements ContainerRequestFilter {
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
final String authorization = requestContext.getHeaderString("Authorization");
if (authorization == null) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("{\"error\":\"missing_authorization_header\"}").build());
return;
}
if (!authorization.toLowerCase(Locale.ENGLISH).startsWith("basic ")) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("{\"error\":\"invalid_authorization_scheme\"}").build());
return;
}
final String credentials = new String(Base64.getDecoder().decode(authorization.substring("basic ".length())), StandardCharsets.UTF_8);
final int sep = credentials.indexOf(':');
if (sep > 0) {
final String user = credentials.substring(0, sep);
final String password = credentials.substring(sep + 1, credentials.length());
final User user = findUser(Account.class, user);
if (user == null || !matches(password, user.getPassword())) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("{\"error\":\"invalid_credentials\"}").build());
return;
}
requestContext.setSecurityContext(new CustomSecurityContext(user));
} else {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("{\"error\":\"invalid_authorization_header_value\"}").build());
}
}
}
What's important there:
- if the login is invalid we abort the request and return directly a UNAUTHORIZED (HTTP 401) response.
- findUser() and matches() are "abstracted" methods in this snippet using any injection allowing to find a user and validating a password (with a salt/hash etc...)
- in the snippet JSON errors are sent using plain string but you can pass an object and rely on JAX-RS serialization
- the most important part will be to set the SecurityContext based on the user which will allow in resource to inject the context with @Context and use it to validate the user:
@GET
public User get(@Context SecurityContext ctx) {
if (!ctx.isUserInRole("user:read")) {
throw new WebApplicationException(Status.FORBIDDEN);
}
return ...;
}
You can even use it with interceptors or other JAX-RS component (since you sort them with priorities) to control the security based on any annotation (like @RolesAllowed).
Now we have our filter and our feature, let's ensure our feature register our filter and makes it active:
@Provider
@Dependent
public class BasicFeature implements DynamicFeature {
@Inject
private MySecurityService service;
@Override
public void configure(final ResourceInfo resourceInfo, final FeatureContext context) {
if (!isSecured(resourceInfo)) {
return;
}
context.register(new BasicFilter(service), Priorities.AUTHENTICATION);
}
}
You can also ignore the priority parameter when registering the filter but you will need to use @Priority on the filter itself to ensure it is registered at the right place in the filter chain.
Finally how to implement the SecurityContext? this part depends your user model but here is a proposal:
class PrincipalImpl implements Principal {
private final String name;
private PrincipalImpl(final String username) {
this.name = username;
}
@Override
public String getName() {
return name;
}
}
private static class AccountSecurityContext implements SecurityContext {
private final User account;
private final SecurityContext securityContext;
private Principal principal;
private AccountSecurityContext(final Account account, final SecurityContext securityContext) {
this.account = account;
this.securityContext = securityContext;
}
@Override
public Principal getUserPrincipal() {
return principal == null ? (principal = new PrincipalImpl(account.getUsername())) : principal;
}
@Override
public boolean isUserInRole(final String role) {
return account.getPermissions().contains(role);
}
@Override
public boolean isSecure() {
return securityContext.isSecure();
}
@Override
public String getAuthenticationScheme() {
return SecurityContext.BASIC_AUTH;
}
}
This implementation assumes the user model owns the permissions/roles but this is actually quite common. The only trick there is to delegate the isSecure() (which says if you use https or not) to the runtime framework to avoid to handle it yourself. Side note on that is if you don't use this method you don't really care and can hardcode true/false.
And here we are, we have:
- a feature registered by the container thanks to @Provider scanning (otherwise register the feature manually when creating the server if you disabled scanning)
- a filter handling basic authentication and overriding the security context to make it available to the rest of the runtime
To open a bit the topic next steps can be to have some contextual security validation. If you re-read the post we mainly treated of the authentication but not much of the authorization. One interesting follow up is to have a filter just after our basic one validating the user has the permissions to invoke the resource endpoint contextually. Here is a sample of API allowing to do that using the same mecanism we dealt with in this post:
@GET
@Path("{id}")
@ValidateAccess(resource = "sensitivedata", identifier = "id")
public SomeData get(@PathParam("id") String id) {
return ...;
}
This kind of API (@ValidateAccess) will allow you to check in an AuthorizationFilter added after the basic one that you have the permission sensitivedata:{id} with the identifier being dynamic. It is very handy to allow only administrators and the user itself to access his data (kind of multi-tenant logic if you want).
Last note will be that this post was about Basic handling because it is easy and the most known mecanism but it is easy to interpolate it to JWT validation or any security mecanism. The hooks are the exact same, just the filter implementation differs a bit.
From the same author:
In the same category: