Welcome back! This is Part 2 of a three-part series on extending Dropwizard to have custom authentication, authorization, and multitenancy. In Part 1, we set up custom authentication. When we left off, we had just used the Java annotations @RolesAllowed
and @PermitAll
to authenticate our resource methods, so they will only run for credentialed users. In this part, we’ll cover Dropwizard authorization. We are going to extend the code we added to check the role assigned to a user and further restrict our methods based on whether that matches.
We can turn role-checking on by enabling another dynamic feature within Jersey. In order for it work, we just need to set up a SecurityContext
object that can tell if a given role applies and set that security context on each incoming request. Most of the code and techniques here are actually a core part of JAX-RS and can be used entirely outside of Dropwizard. All of the example code I’m going to show in my series lives in this repo if you want to follow along.
Enabling Role-Checking
To make Jersey check role annotations before each request, we need to enable the RolesAllowedDynamicFeature
, which is a core part of Jersey, not Dropwizard. We can enable it in our app like so:
environment.jersey().register(RolesAllowedDynamicFeature.class);
If you just activate that, you’ll notice that you can no longer use any of the endpoints annotated with @RolesAllowed
(though those with @PermitAll
still work). These endpoints will return a 403, because they have no way to validate their set of roles against the logged-in user. Fixing that is our next step.
Custom Security Context
A SecurityContext
is a core JAX-RS object that is attached to a request context for the purposes of validating security. We are going to modify our auth filter to attach a security context to each authenticated request. The security context we attach will have logic to check the roles in the @RolesAllowed
annotation against the authenticated user (to which we will also add a role field).
First off, we need to create a custom subclass of SecurityContext
that can check our roles:
public class CustomSecurityContext implements SecurityContext {
private final CustomAuthUser principal;
private final SecurityContext securityContext;
public CustomSecurityContext(CustomAuthUser principal, SecurityContext securityContext) {
this.principal = principal;
this.securityContext = securityContext;
}
@Override
public Principal getUserPrincipal() {
return principal;
}
@Override
public boolean isUserInRole(String role) {
return role.equals(principal.getRole().name());
}
@Override
public boolean isSecure() {
return securityContext.isSecure();
}
@Override
public String getAuthenticationScheme() {
return "CUSTOM_TOKEN";
}
}
The most important part of this is the isUserInRole
method which drives our Dropwizard authorization code. It will be called once for each role we define in our @RolesAllowed
annotation, and if it returns “true” for any of them, we are authorized to use the method.
Now we need to update our auth filter method to attach a security context whenever we authenticate a user. We don’t have to do anything when there is no authenticated user, because the default security context has no user attached and will fail authorization checks. We also need to make sure to set the @Prioroity
of our auth filter to Priorities.AUTHENTICATION
so this code will run before any other filters that depend on authentication.
@PreMatching
@Priority(Priorities.AUTHENTICATION)
public class CustomAuthFilter extends AuthFilter<CustomCredentials, CustomAuthUser> {
private CustomAuthenticator authenticator;
public CustomAuthFilter(CustomAuthenticator authenticator) {
this.authenticator = authenticator;
}
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Optional<CustomAuthUser> authenticatedUser;
try {
CustomCredentials credentials = getCredentials(requestContext);
authenticatedUser = authenticator.authenticate(credentials);
} catch (AuthenticationException e) {
throw new WebApplicationException("Unable to validate credentials", Response.Status.UNAUTHORIZED);
}
if (authenticatedUser.isPresent()) {
SecurityContext securityContext = new CustomSecurityContext(authenticatedUser.get(), requestContext.getSecurityContext());
requestContext.setSecurityContext(securityContext);
} else {
throw new WebApplicationException("Credentials not valid", Response.Status.UNAUTHORIZED);
}
}
...
}
Dropwizard Authorization Complete
With the AuthDynamicFeature
enabled and our security context attached to authenticated requests, we now have role-based authentication on every incoming request. If you’ve been following Parts 1 and 2 of this post, you’ll see that we have both Dropwizard authentication and Dropwizard authorization for our API. This is probably enough for many apps, but in Part 3, I’ll show you how you can also add multitenancy to a Dropwizard application using a similar annotation-based approach.
You can see the code for just what we’ve done for Parts 1 and 2 here and the complete code for all three parts here.