This is Part 1 of a three-part series on extending Dropwizard with custom authentication, authorization, and multitenancy. For Part 1, we are going to go over adding custom authentication to Dropwizard.
If you don’t already know, Dropwizard is an awesome Java web API framework. It is my preferred web stack. I’ve written about it previously in: Serving Static Assets with DropWizard, Using Hibernate DAOs in DropWizard Tasks, and Hooking up Custom Jersey Servlets in Dropwizard (note that some of those posts are out-of-date for newer versions of Dropwizard).
It already comes with out-of-the-box support for http basic authentication and OAuth in the dropwizard-auth
package. However, in a recent project, I needed to integrate a Dropwizard API with an existing API from another platform using a custom authentication scheme. Fortunately for me, Dropwizard exposes a set of extendible privative for authentication that I was able to leverage in my solution. All of the example code I’m going to share in my posts lives in this repo, if you want to follow along.
Disclaimers
Before we begin, I want to share a couple of disclaimers:
1. This was written for Dropwizard 0.9.x (the current version as of this writing). Future changes to Dropwizard may alter or invalidate details of this post. Be aware if you are using a later version.
2. This post uses hibernate for the database integration. While much of what we do here is possible with JDBI or other database integrations, I am not going to cover or discuss those. I will likely not be able to answer questions about them.
Now let’s get to work.
Adding Auth Annotations
Dropwizard allows us to use the role annotations under the javax.annotation.security
package, @RolesAllowed
, @PermitAll
, and @DenyAll
to enforce authentication on our resource methods. You can add them to each method to set the permissions like so:
@POST
@UnitOfWork
@RolesAllowed({"MANAGER"})
public Widget createWidget(Widget widget) {
WidgetModel widgetModel = widgetDAO.createWidget(widget);
return new Widget(widgetModel.getId(), widgetModel.getName(), widgetModel.getScope());
}
@GET
@Path("/public")
@UnitOfWork
@PermitAll
public List getPublicWidgets() {
return getWidgetsForScope(WidgetScope.PUBLIC);
}
@GET
@Path("/private")
@UnitOfWork
@RolesAllowed({"EMPLOYEE", "MANAGER"})
public List getPrivateWidgets() {
return getWidgetsForScope(WidgetScope.PRIVATE);
}
@GET
@Path("/top-secret")
@UnitOfWork
@RolesAllowed({"MANAGER"})
public List getTopSecretWidgets() {
return getWidgetsForScope(WidgetScope.TOP_SECRET);
}
In the above example, you can see that only users with the MANAGER
role are allowed to create new widgets or view top-secret widgets. Users with an EMPLOYEE
or MANAGER
roles can see internal widgets, and anyone can see public widgets. It is important to note that if we don’t put any of these annotations on to a resource method, it will be open to anyone by default. This is different from the @PermitAll
annotation, which still authenticates a user and just disregards what roles they have. To protect against this, I usually use reflection to write a test like the one here that ensures every resource method has one of these annotations.
If you just add these roles and start up your application, you are going to be disappointed. These annotations don’t have any handler by default, so we are going to need to add an AuthFilter
to Dropwizard to make them do something.
Adding an AuthFilter
In order to make our annotations do something, we need to use the tools in the dropwizard-auth
package (which you’ll need to add to your dependency list). If we were just using HTTP basic auth or OAuth, we could use the out-of-the-box tools for those and hook them up to a role schema. However, since we are hooking into a custom authentication scheme, we are going to have to create our own stuff using the package primitives.
The first thing we need to create is a Jersey filter that will run before each request and execute our authentication code. Dropwizard provides a convenient base class called AuthFilter
that will do the job. A bare-bones AuthFilter
is parameterized to a type of credentials and security principal and would look like this:
@PreMatching
@Priority(Priorities.AUTHENTICATION)
public class CustomAuthFilter extends AuthFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
}
We can register the filter inside our Dropwizard application’s run method using the AuthDynamicFeature
like so:
CustomAuthFilter filter = new CustomAuthFilter();
environment.jersey().register(new AuthDynamicFeature(filter));
Now our filter will run before each request annotated with @RolesAllowed
, @PermitAll
, or @DenyAll
to authenticate the user. Right now, though, our filter just rejects every request with a 401 status code. The next thing we need to do is add an Authenticator
which will actually run the logic of authenticating a user’s credentials.
Adding an Authenticator
Our authenticator exposes a single method, authenticate, which takes in a CustomCredentials
as an argument. The authenticator then uses the userId and token in the credentials to authenticate the user against the token stored in our database for that user. If the token matches, we return the user wrapped as an optional—otherwise, we return an empty optional. Also note that since we are using hibernate, we need to new up our authenticator using UnitOfWorkProxyFactory
like so:
CustomAuthenticator authenticator = new UnitOfWorkAwareProxyFactory(hibernate)
.create(CustomAuthenticator.class, new Class>[]{TokenDAO.class, UserDAO.class}, new Object[]{tokenDAO, userDAO});
And our whole authenticator looks like this:
public class CustomAuthenticator implements Authenticator {
private TokenDAO tokenDAO;
private UserDAO userDAO;
public CustomAuthenticator(TokenDAO tokenDAO, UserDAO userDAO) {
this.tokenDAO = tokenDAO;
this.userDAO = userDAO;
}
@Override
@UnitOfWork
public Optional authenticate(CustomCredentials credentials) throws AuthenticationException {
CustomAuthUser authenticatedUser = null;
Optional user = userDAO.getUser(credentials.getUserId());
if (user.isPresent()) {
Optional token = tokenDAO.findTokenForUser(user.get());
if (token.isPresent()) {
TokenModel tokenModel = token.get();
if (tokenModel.getId().equals(credentials.getToken())) {
authenticatedUser = new CustomAuthUser(tokenModel.getUser().getId(), tokenModel.getUser().getName());
}
}
}
return Optional.fromNullable(authenticatedUser);
}
}
Now we just need to hook up our CustomAuthFilter
to our CustomAuthenticator
by calling the authenticate method with some credentials. We’ll create the credentials in our auth filter by parsing our request context. (In our case, this means pulling the credentials out of cookies.)
public class CustomAuthFilter extends AuthFilter {
private CustomAuthenticator authenticator;
public CustomAuthFilter(CustomAuthenticator authenticator) {
this.authenticator = authenticator;
}
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Optional 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()) {
throw new WebApplicationException("Credentials not valid", Response.Status.UNAUTHORIZED);
}
}
private CustomCredentials getCredentials(ContainerRequestContext requestContext) {
CustomCredentials credentials = new CustomCredentials();
try {
String rawToken = requestContext
.getCookies()
.get("auth_token")
.getValue();
String rawUserId = requestContext
.getCookies()
.get("auth_user")
.getValue();
credentials.setToken(UUID.fromString(rawToken));
credentials.setUserId(Long.valueOf(rawUserId));
} catch (Exception e) {
throw new WebApplicationException("Unable to parse credentials", Response.Status.UNAUTHORIZED);
}
return credentials;
}
}
Notice that we parse the credentials out of the request cookies and create a CustomCredentials
instance. We then pass those credentials to the CustomAuthenticator
in the filter
method. If the authenticator returns a user, we are properly authenticated. Otherwise, we abort the request with a 401 error.
Custom Authentication Complete
And with that, we now have custom authentication on all of our annotated resource methods. None of them can be successfully called without a valid userId and token combination in the request cookies.
But what about those roles that we listed in the method with the @RolesAllowed
annotation? Right now, any of the methods are open to any authenticated user. To check the roles on the user requires a little more work on our part, which we will cover in Part 2.
You can see the code for Part 1 here and the complete code for all three parts here.