Using Jetty form-based authentication with Dropwizard

Dropwizard is a rather nice Java REST framework. Although mainly designed for web services, it can also be used for MVC-style web applications, with dropwizard-views which supports both FreeMarker and Mustache templates on the front end.

What’s missing out of the box is form-based authentication. The embedded Jetty web server does contain this functionality — but there are a number of steps involved in getting it wired up.

First, create /login and /logout pages

Let’s create a simple Jersey resource for each page. We’ll also create a /login/error page to handle failed logins.

@Path("/login")
public class LoginResource {

	@GET
	public View login() {
		return new LoginView();
	}
	
	@GET @Path("error")
	public String error() {
		return "Error logging in.";
	}
}

@Path("/logout")
public class LogoutResource {

	@GET
	public String logout(@Context HttpServletRequest req) {
		
		req.getSession().invalidate();
		return "You have been logged out.";
	}
}

The login page needs to post to the special /j_security_check endpoint, so your login view should look something like this:

<html>
<head><title>Log in</title></head>
<body>

<form method='POST' action='/j_security_check'>
  <input type='text' name='j_username'/>
  <input type='password' name='j_password'/>
  <input type='submit' value='Login'/>
</form>

</body>
</html>

Automatically redirect 403 Forbidden responses to the login page

HTTP 403 responses are represented by the JAX-RS class ForbiddenException.

To handle these, we need an ExceptionMapper that will store the target URI in session and then redirect the user to the login page. Once they have logged in, Jetty will retrieve the URI and forward the user to their original destination.

public class ForbiddenExceptionMapper implements ExceptionMapper<ForbiddenException> {
	
	private UriInfo ui;
	private HttpServletRequest req;
	
	public ForbiddenExceptionMapper(@Context UriInfo ui, @Context HttpServletRequest req) {
		this.ui = ui;
		this.req = req;
	}

	@Override public Response toResponse(ForbiddenException e) {

		String location = ui.getPath();
		
		if (location != null) {
			req.getSession().setAttribute(FormAuthenticator.__J_URI, location);
		}
		return Response.temporaryRedirect(URI.create("/login")).build();
	}
	
}

Create a new server factory

To work, the auth service needs to be initialised at the correct point in Jetty’s lifecycle; after the server has been created but before the app servlet is added. We’ll need a custom ServerFactory so we can hook into Jetty at the right time.

@JsonTypeName("jettyauthserver")
public class JettyAuthServerFactory extends DefaultServerFactory {

    @Override
    protected Handler createAppServlet(Server server, JerseyEnvironment jersey,
        ObjectMapper objectMapper, Validator validator, MutableServletContextHandler handler,
        Servlet jerseyContainer, MetricRegistry metricRegistry) {

        setupJettyAuth(handler);

        return super.createAppServlet(server, jersey, objectMapper, validator, handler,
            jerseyContainer, metricRegistry);
    }

    private static void setupJettyAuth(MutableServletContextHandler context) {
    	
    	context.setSessionHandler(new SessionHandler());
    	
    	Constraint constraint = new Constraint();
    	constraint.setName(Constraint.__FORM_AUTH);
    	constraint.setRoles(new String[]{"user","admin","moderator"});
    	constraint.setAuthenticate(true);

    	ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();

    	HashLoginService loginService = new HashLoginService();
    	loginService.putUser("defaultuser", new Password("defaultpass"), new String[] {"admin"});
    	
    	FormAuthenticator authenticator = new FormAuthenticator("/login", "/login/error", false);
    	securityHandler.setAuthenticator(authenticator);

    	context.setSecurityHandler(securityHandler);
    }

}

Note: this example authenticates using a fixed, in-memory list of roles and user names. For a real application, you’ll want to actually use a database.

To do this, replace the HashLoginService with a JDBCLoginService (or your own login service implementation).

Register the server factory

Create a text file called io.dropwizard.server.ServerFactory and save it in src/main/java/resources/META-INF/services/

The file needs to contain the fully qualified name of the new server factory. For example:

com.example.JettyAuthServerFactory

Finally, register the server name in your application YAML config:

server:
  type:
    jettyauthserver

Application setup

To hook everything up, these lines need to go in the run() method of your Dropwizard application.

// Enable the Jersey security annotations on resources 
environment.jersey().getResourceConfig().register(RolesAllowedDynamicFeature.class);

// Register custom exception mapper to redirect 403 errors to the login page
environment.jersey().register(ForbiddenExceptionMapper.class);

// Register the Login and Logout resources
environment.jersey().register(new LoginResource());
environment.jersey().register(new LogoutResource());

Try it out

With all the pieces in place, you can now use Jersey’s declarative security annotations to protect resources:

@Path("/topsecret")
@RolesAllowed("admin")
public class TopSecretResource {

    /* ... */

}