Dropwizard Authentication
The dropwizard-auth
client provides authentication using either HTTP Basic
Authentication or OAuth2 bearer tokens.
Authenticators
An authenticator is a strategy class which, given a set of client-provided credentials, possibly returns a principal (i.e., the person or entity on behalf of whom your service will do something).
Authenticators implement the Authenticator<C, P extends Principal>
interface, which has a single method:
public class ExampleAuthenticator implements Authenticator<BasicCredentials, User> {
@Override
public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException {
if ("secret".equals(credentials.getPassword())) {
return Optional.of(new User(credentials.getUsername()));
}
return Optional.empty();
}
}
This authenticator takes basic auth credentials and if the client-provided
password is secret
, authenticates the client as a User
with the client-provided username.
If the password doesn’t match, an absent Optional
is returned instead, indicating that the
credentials are invalid.
Warning
It’s important for authentication services not to provide too much information in their
errors. The fact that a username or email has an account may be meaningful to an
attacker, so the Authenticator
interface doesn’t allow you to distinguish between
a bad username and a bad password. You should only throw an AuthenticationException
if the authenticator is unable to check the credentials (e.g., your database is
down).
Caching
Because the backing data stores for authenticators may not handle high throughput (an RDBMS or LDAP server, for example), Dropwizard provides a decorator class which provides caching:
SimpleAuthenticator simpleAuthenticator = new SimpleAuthenticator();
CachingAuthenticator<BasicCredentials, User> cachingAuthenticator = new CachingAuthenticator<>(
metricRegistry, simpleAuthenticator,
config.getAuthenticationCachePolicy());
Dropwizard can parse Caffeine’s CaffeineSpec
from the configuration policy, allowing your
configuration file to look like this:
authenticationCachePolicy: maximumSize=10000, expireAfterAccess=10m
This caches up to 10,000 principals, evicting stale entries after 10 minutes.
Basic Authentication
The AuthDynamicFeature
with the BasicCredentialAuthFilter
and RolesAllowedDynamicFeature
enables HTTP Basic authentication and authorization; requires an authenticator which
takes instances of BasicCredentials
. If you don’t use authorization, then RolesAllowedDynamicFeature
is not required.
@Override
public void run(ExampleConfiguration configuration,
Environment environment) {
environment.jersey().register(new AuthDynamicFeature(
new BasicCredentialAuthFilter.Builder<User>()
.setAuthenticator(new ExampleAuthenticator())
.setAuthorizer(new ExampleAuthorizer())
.setRealm("SUPER SECRET STUFF")
.buildAuthFilter()));
environment.jersey().register(RolesAllowedDynamicFeature.class);
//If you want to use @Auth to inject a custom Principal type into your resource
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
}
OAuth2
The AuthDynamicFeature
with OAuthCredentialAuthFilter
and RolesAllowedDynamicFeature
enables OAuth2 bearer-token authentication and authorization; requires an authenticator which
takes instances of String
. If you don’t use authorization, then RolesAllowedDynamicFeature
is not required.
@Override
public void run(ExampleConfiguration configuration,
Environment environment) {
environment.jersey().register(new AuthDynamicFeature(
new OAuthCredentialAuthFilter.Builder<User>()
.setAuthenticator(new ExampleOAuthAuthenticator())
.setAuthorizer(new ExampleAuthorizer())
.setPrefix("Bearer")
.buildAuthFilter()));
environment.jersey().register(RolesAllowedDynamicFeature.class);
//If you want to use @Auth to inject a custom Principal type into your resource
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
}
Chained Factories
The ChainedAuthFilter
enables usage of various authentication factories at the same time.
@Override
public void run(ExampleConfiguration configuration,
Environment environment) {
AuthFilter basicCredentialAuthFilter = new BasicCredentialAuthFilter.Builder<>()
.setAuthenticator(new ExampleBasicAuthenticator())
.setAuthorizer(new ExampleAuthorizer())
.setPrefix("Basic")
.buildAuthFilter();
AuthFilter oauthCredentialAuthFilter = new OAuthCredentialAuthFilter.Builder<>()
.setAuthenticator(new ExampleOAuthAuthenticator())
.setAuthorizer(new ExampleAuthorizer())
.setPrefix("Bearer")
.buildAuthFilter();
List<AuthFilter> filters = Lists.newArrayList(basicCredentialAuthFilter, oauthCredentialAuthFilter);
environment.jersey().register(new AuthDynamicFeature(new ChainedAuthFilter(filters)));
environment.jersey().register(RolesAllowedDynamicFeature.class);
//If you want to use @Auth to inject a custom Principal type into your resource
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
}
For this to work properly, all chained factories must produce the same type of principal, here User
.
Protecting Resources
There are two ways to protect a resource. You can mark your resource method with one of the following annotations:
@PermitAll
. All authenticated users will have access to the method.@RolesAllowed
. Access will be granted to the users with the specified roles.@DenyAll
. No access will be granted to anyone.
Note
You can use @RolesAllowed
, @PermitAll
on the class level. Method annotations take precedence over the class ones.
Alternatively, you can annotate the parameter representing your principal with @Auth
. Note you must register a
jersey provider to make this work.
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
@RolesAllowed("ADMIN")
@GET
public SecretPlan getSecretPlan(@Auth User user) {
return dao.findPlanForUser(user);
}
You can also access the Principal by adding a parameter to your method @Context SecurityContext context
. Note this
will not automatically register the servlet filter which performs authentication. You will still need to add one of
@PermitAll
, @RolesAllowed
, or @DenyAll
. This is not the case with @Auth
. When that is present, the auth
filter is automatically registered to facilitate users upgrading from older versions of Dropwizard
@RolesAllowed("ADMIN")
@GET
public SecretPlan getSecretPlan(@Context SecurityContext context) {
User userPrincipal = (User) context.getUserPrincipal();
return dao.findPlanForUser(user);
}
If there are no provided credentials for the request, or if the credentials are invalid, the
provider will return a scheme-appropriate 401 Unauthorized
response without calling your
resource method.
Optional protection
Resource methods can be _optionally_ protected by representing the
principal as an Optional
. In such cases, the Optional
resource
method argument will be populated with the principal, if
present. Otherwise, the argument will be Optional.empty
.
For instance, say you have an endpoint that should display a logged-in user’s name, but return an anonymous reply for unauthenticated requests. You need to implement a custom filter which injects a security context containing the principal if it exists, without performing authentication.
@GET
public String getGreeting(@Auth Optional<User> userOpt) {
if (userOpt.isPresent()) {
return "Hello, " + userOpt.get().getName() + "!";
} else {
return "Greetings, anonymous visitor!"
}
}
For optionally-protected resources, requests with invalid auth will be treated the same as those with no provided auth credentials. That is to say, requests that _fail_ to meet an authenticator or authorizer’s requirements result in an empty principal being passed to the resource method.
Testing Protected Resources
Add this dependency into your pom.xml
file:
<dependencies>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey.version}</version>
<exclusions>
<exclusion>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
OAuth Example
When you build your ResourceExtension
, add the GrizzlyWebTestContainerFactory
line.
@ExtendWith(DropwizardExtensionsSupport.class)
public class OAuthResourceTest {
public ResourceExtension resourceExtension = ResourceExtension
.builder()
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addProvider(new AuthDynamicFeature(new OAuthCredentialAuthFilter.Builder<User>()
.setAuthenticator(new MyOAuthAuthenticator())
.setAuthorizer(new MyAuthorizer())
.setRealm("SUPER SECRET STUFF")
.setPrefix("Bearer")
.buildAuthFilter()))
.addProvider(RolesAllowedDynamicFeature.class)
.addProvider(new AuthValueFactoryProvider.Binder<>(User.class))
.addResource(new ProtectedResource())
.build();
}
Note that you need to set the token header manually.
@Test
public void testProtected() throws Exception {
final Response response = resourceExtension.target("/protected")
.request(MediaType.APPLICATION_JSON_TYPE)
.header("Authorization", "Bearer TOKEN")
.get();
assertThat(response.getStatus()).isEqualTo(200);
}
BasicAuth Example
When you build your ResourceExtension
, add the GrizzlyWebTestContainerFactory
line.
@ExtendWith(DropwizardExtensionsSupport.class)
public class OAuthResourceTest {
public ResourceExtension resourceExtension = ResourceExtension
.builder()
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addProvider(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<User>()
.setAuthenticator(new MyBasicAuthenticator())
.setAuthorizer(new MyBasicAuthorizer())
.buildAuthFilter()))
.addProvider(RolesAllowedDynamicFeature.class)
.addProvider(new AuthValueFactoryProvider.Binder<>(User.class))
.addResource(new ProtectedResource())
.build()
}
Note that you need to set the authorization header manually.
@Test
public void testProtectedResource(){
String credential = "Basic " + Base64.getEncoder().encodeToString("test@gmail.com:secret".getBytes())
Response response = resourceExtension
.target("/protected")
.request()
.header(HttpHeaders.AUTHORIZATION, credential)
.get();
Assert.assertEquals(200, response.getStatus());
}
Multiple Principals and Authenticators
In some cases you may want to use different authenticators/authentication schemes for different resources. For example you may want Basic authentication for one resource and OAuth for another resource, at the same time using a different Principal for each authentication scheme.
For this use case, there is the PolymorphicAuthDynamicFeature
and the
PolymorphicAuthValueFactoryProvider
. With these two components, we can use different
combinations of authentication schemes/authenticators/authorizers/principals. To use this
feature, we need to do a few things:
Register the
PolymorphicAuthDynamicFeature
with a map that maps principal types to authentication filters.Register the
PolymorphicAuthValueFactoryProvider
with a set of principal classes that you will be using.Annotate your resource method
Principal
parameters with@Auth
.
As an example, the following code configures both OAuth and Basic authentication, using a different principal for each.
final AuthFilter<BasicCredentials, BasicPrincipal> basicFilter
= new BasicCredentialAuthFilter.Builder<BasicPrincipal>()
.setAuthenticator(new ExampleAuthenticator())
.setRealm("SUPER SECRET STUFF")
.buildAuthFilter());
final AuthFilter<String, OAuthPrincipal> oauthFilter
= new OAuthCredentialAuthFilter.Builder<OAuthPrincipal>()
.setAuthenticator(new ExampleOAuthAuthenticator())
.setPrefix("Bearer")
.buildAuthFilter());
final PolymorphicAuthDynamicFeature feature = new PolymorphicAuthDynamicFeature<>(
ImmutableMap.of(
BasicPrincipal.class, basicFilter,
OAuthPrincipal.class, oauthFilter));
final AbstractBinder binder = new PolymorphicAuthValueFactoryProvider.Binder<>(
ImmutableSet.of(BasicPrincipal.class, OAuthPrincipal.class));
environment.jersey().register(feature);
environment.jersey().register(binder);
Now we are able to do something like the following
@GET
public Response basicAuthResource(@Auth BasicPrincipal principal) {}
@GET
public Response oauthResource(@Auth OAuthPrincipal principal) {}
The first resource method will use Basic authentication while the second one will use OAuth.
Note that with the above example, only authentication is configured. If you also want authorization, the following steps will need to be taken.
Register the
RolesAllowedDynamicFeature
with the application.Make sure you add
Authorizers
when you build yourAuthFilters
.Make sure any custom
AuthFilter
you add has the@Priority(Priorities.AUTHENTICATION)
annotation set (otherwise authorization will be tested before the request’s security context is properly set and will fail).Annotate the resource method with the authorization annotation. Unlike the note earlier in this document that says authorization annotations are allowed on classes, with this poly feature, currently that is not supported. The annotation MUST go on the resource method
So continuing with the previous example you should add the following configurations
... = new BasicCredentialAuthFilter.Builder<BasicPrincipal>()
.setAuthorizer(new ExampleAuthorizer()).. // set authorizer
... = new OAuthCredentialAuthFilter.Builder<OAuthPrincipal>()
.setAuthorizer(new ExampleAuthorizer()).. // set authorizer
environment.jersey().register(RolesAllowedDynamicFeature.class);
Now we can do
@GET
@RolesAllowed({ "ADMIN" })
public Response baseAuthResource(@Auth BasicPrincipal principal) {}
@GET
@RolesAllowed({ "ADMIN" })
public Response oauthResource(@Auth OAuthPrincipal principal) {}
Note
The polymorphic auth feature SHOULD NOT be used with any other AuthDynamicFeature
. Doing so may have undesired effects.