Optional Authentication with Dropwizard

May 18, 2016

Dropwizard provides a great framework for authentication & authorization. Authenticators do what their name implies, returning a Principal (probably your User object) that servlets can use for building responses. The Authorizer interface has a single methoed, authorize(), which takes a Principal and a string role to authorize access for. These get wrapped in an AuthFilter which extracts credentials from the requst and passed on to the Authenticator.

With the authen & authz classes in place protecting resources is easy: you simply annotate them with one of @PermitAll, @RolesAllowed, or @DenyAll. The last one does exactly what it says on the tin. A specific role or set of roles can be permitted access with the @RolesAllowed annotation, to which you pass a String or String[] of roles. @PermitAll allows any authenticated user to access the resource. What is missing here is an annotation to allow optionally authenticated resources—allowing you to customize a response for a known user but deliver a generic response to anonymous visitors.

Optionally protected resources

The Dropwizard manual gives a cursory explanation of how to implement optional authentication:

If you have a resource which is optionally protected (e.g., you want to display a logged-in user’s name but not require login), you need to implement a custom filter which injects a security context containing the principal if it exists, without performing authentication.

The process for optional resources involves two AuthFilters: one to check & process credentials for a logged-in user and a second that provides a default user. These can be hit in turn with a ChainedAuthFilter.

I’ll show the important parts of how I accomplished this with code examples written in a mix of Java and Kotlin.

Wiring

Setting up Dropwizard’s authentication involves creating an AuthFilter to which you pass the Authenticator and Authorizer that it will use. Creating a ChainedAuthFilter is easy, just pass a List<AuthFilter> with the filters in the order they should be executed. Dropwizard tries each of the AuthFilters in turn until one returns successfully.

In the application’s run() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Application.java
ApiKeyAuthFilter apiKey = new ApiKeyAuthFilter.Builder()
        .setAuthenticator(apiKeyAuthenticator)
        .setAuthorizer(authorizer)
        .setPrefix("API key")
        .buildAuthFilter();
DefaultAuthFilter default = new DefaultAuthFilter.Builder()
        .setAuthenticator(defaultAuthenticator)
        .setAuthorizer(authorizer)
        .setPrefix("default")
        .buildAuthFilter();

List<AuthFilter> filterList = Lists.newArrayList(apiKey, default);
ChainedAuthFilter chainedAuthFilter = new ChainedAuthFilter<>(filterList)

environment.jersey().register(new AuthDynamicFeature(chainedAuthFilter));
environment.jersey().register(RolesAllowedDynamicFeature.class);

The AuthFilters and their respective Authenticators are described below.

API key authentication

As mentioned, my user authentication is done with an API key that is passed in the Authorization HTTP header. The filter extracts the value and passes it to the authenticate() method of ApiKeyAuthenticator.

1
2
3
4
5
6
7
// ApiKeyAuthFilter.kt
override fun filter(requestContext: ContainerRequestContext) {
    val credentials = requestContext.headers.getFirst(HttpHeaders.AUTHORIZATION)
    if (!authenticate(requestContext, credentials, API_KEY_AUTH)) {
        throw WebApplicationException(unauthHandler.buildResponse(prefix, realm))
    }
}

The API key authenticator checks the databse to see if the given API key exists. If the key is found, the matching User is returned; if not found, an empty Optional is returned instead.

1
2
3
4
5
6
7
8
9
10
11
// ApiKeyAuthenticator.kt
@Throws(AuthenticationException::class)
override fun authenticate(credentials: ApiKey): Optional<User> {
    val userId = apiKeyDao.getUserIdForAccessToken(credentials.accessToken)
    if (userId != 0) {
        val user = userDao.getUser(userId)
        return Optional.of(user)
    }

    return Optional.empty<User>()
}

Default authentication

If API key authentication fails, either because the user provided invalid credentials or no credentials at all, then the next AuthFilter configured in the ChainedAuthFilter is invoked. Authentication for the default user doesn’t actually check anything, so Unit is passed instead of credentials:

1
2
3
4
5
6
// DefaultAuthFilter.kt
override fun filter(requestContext: ContainerRequestContext) {
    if (!authenticate(requestContext, Unit, "DEFAULT")) {
        throw WebApplicationException(unauthHandler.buildResponse(prefix, realm))
    }
}

As the last authenticator to run in the chain, the DefaultAuthenticator never fails, it simply returns a default-constructed User object.

1
2
3
4
5
6
7
8
// DefaultAuthenticator.kt
@Throws(AuthenticationException::class)
override fun authenticate(credentials: Unit): Optional<User> {
    logger.debug("Using default auth");

    val user = User()
    return Optional.of(user)
}

Usage in servlets

The User object looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// User.kt
data class User(val id: Int, roles: List<Role>) : Principal {
    constructor() : this(0, emptyList())

    init {
        var theRoles = mutableListOf<Role>()

        if (id != 0) theRoles.add(Role.USER)

        roles = theRoles.toList()
    }

    fun hasRole(role: Role): Boolean {
        return roles.contains(role)
    }
}

Which allows me to check whether valid authentication was provided within a servlet:

1
2
3
4
5
6
7
8
9
10
// SomeResource.kt
fun optionallyAuthenticatedResource(@Context context: SecurityContext) {
    user = context.getUserPrincipal()

    if (user != null && user.hasRole(Role.USER)) {
        // Do something for authenticated users
    }

    // Do other stuff for all users
}