Skip to content

Instantly share code, notes, and snippets.

@adamkewley
Created December 22, 2016 09:43
Show Gist options
  • Save adamkewley/1746c0be20fee193e55bfc9137e7c1ec to your computer and use it in GitHub Desktop.
Save adamkewley/1746c0be20fee193e55bfc9137e7c1ec to your computer and use it in GitHub Desktop.
Dropwizard JWT Authentication Filter
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.Authorizer;
import javax.annotation.Priority;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.SecurityContext;
import java.io.IOException;
import java.security.Principal;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An authentication filter that plucks JWT token strings from requests'
* "Authorization:" header to authorize incoming requests.
*/
@Priority(1000)
public final class JwtAuthFilter<P extends Principal> implements ContainerRequestFilter {
private static final Pattern AUTHORIZATION_HEADER_PATTERN = Pattern.compile("Bearer ([^.]+\\.[^.]+\\.[^.]+)");
private static final String AUTHENTICATION_SCHEME_NAME = "JWT";
private final Authenticator<String, P> jwtAuthenticator;
private final Authorizer<P> authorizer;
/**
* Construct an instance of JwtAuthFilter.
*
* @param jwtAuthenticator An authenticator that authenticates users using a JWT string.
* @param authorizer An authorizer for principals returned by the authenticator.
*
* @throws NullPointerException If jwtAuthenticator or authorizer are null.
*/
public JwtAuthFilter(Authenticator<String, P> jwtAuthenticator, Authorizer<P> authorizer)
throws NullPointerException {
Objects.requireNonNull(jwtAuthenticator);
Objects.requireNonNull(authorizer);
this.jwtAuthenticator = jwtAuthenticator;
this.authorizer = authorizer;
}
/**
* Filter an incoming request.
*
* @param containerRequestContext The incoming request.
*
* @throws NullPointerException If containerRequestContext is null.
* @throws WebApplicationException If an authentication error occurred.
* @throws IOException If an internal error occurred.
*/
@Override
public void filter(ContainerRequestContext containerRequestContext) throws WebApplicationException, IOException {
final String authorizationHeaderValue = containerRequestContext.getHeaderString("Authorization");
if (authorizationHeaderValue == null) throw new WebApplicationException("Authorization header was not set.", 401);
final String jwtToken = tryExtractJwtTokenFromAuthorizationHeader(authorizationHeaderValue);
final P principal = this.tryAuthenticateUsingJwtToken(jwtToken);
final boolean requestIsSecure = isRequestSecure(containerRequestContext);
final SecurityContext securityContext = new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return principal;
}
@Override
public boolean isUserInRole(String s) {
return JwtAuthFilter.this.authorizer.authorize(principal, s);
}
@Override
public boolean isSecure() {
return requestIsSecure;
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME_NAME;
}
};
containerRequestContext.setSecurityContext(securityContext);
}
/**
* Returns true if the request is secure.
*
* @param request The request to test.
* @return True if the request is secure; otherwise, false.
*/
private static boolean isRequestSecure(ContainerRequestContext request) {
final SecurityContext securityContext = request.getSecurityContext();
return securityContext != null && securityContext.isSecure();
}
/**
* Try to authenticate the supplied jwtToken, returning an
* authenticated principal if successful.
*
* @param jwtToken The token to authenticate.
* @return An authenticated principal.
* @throws WebApplicationException If the token was invalid or
* could not be authenticated.
*/
private P tryAuthenticateUsingJwtToken(String jwtToken) throws WebApplicationException {
try {
Optional<P> possiblePrincipal = this.jwtAuthenticator.authenticate(jwtToken);
if (possiblePrincipal.isPresent()) return possiblePrincipal.get();
else throw new WebApplicationException("Authentication failed for the supplied json web token.", 401);
} catch (AuthenticationException ex) {
throw new WebApplicationException("Authentication failed for the supplied json web token.", 401);
}
}
/**
* Extract a JWT token from a Bearer-style Authorization header.
*
* @param authorizationHeader The value of the Authorization header (e.g. "Bearer 0x9d8.23fjd3.0XrfkK")
* @return The extracted JWT token.
* @throws WebApplicationException If the value of the Authorization header was not
* as expected.
*/
private static String tryExtractJwtTokenFromAuthorizationHeader(String authorizationHeader)
throws WebApplicationException {
final Matcher patternMatcher = AUTHORIZATION_HEADER_PATTERN.matcher(authorizationHeader);
final Boolean headerHasCorrectFormat = patternMatcher.matches();
if (headerHasCorrectFormat) {
final String jwtToken = patternMatcher.group(1);
return jwtToken;
} else throw new WebApplicationException("The format of the Authorization header was invalid.", 401);
}
}
@adamkewley
Copy link
Author

This is a pretty rudimentary implementation of a container request filter that looks for Authrorization: Bearer TOKEN headers attached to web requests that hit standard Dropwizard servers. The request filter essentially plucks the JWT token from the header, validates that the token is syntactically valid, and passes the raw JWT token string to the authenticator.

It is assumed that the authenticator will handle secret key and principal checking on the raw JWT string. This is so that implementers can implement their caching, token signing, and authentication methods however they want in their own Dropwizard authenticator - this class literally only handles the request layer of JWT handling.

I implemented this against the ContainerRequestFilter interface rather than Dropwizard's abstract filter classes because I prefer implementing my own interfaces (I find them easier to reason with).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment