so, for some background, the reason you should only throw an AuthenticationException
is because the filter provided by the good guys at DW only handles this exception. For all other exceptions, this is undefined.
For detail see: AuthFilter#authenticate
However, there is a really easy way to do what you want.
Looking at the DW code, you get this:
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
final BasicCredentials credentials =
getCredentials(requestContext.getHeaders().getFirst(HttpHeaders.AUTHORIZATION));
if (!authenticate(requestContext, credentials, SecurityContext.BASIC_AUTH)) {
throw new WebApplicationException(unauthorizedHandler.buildResponse(prefix, realm));
}
}
What this code does is to call your Authenticator
to resolve the user and then check if it is authenticated. If it is not, then it will call the UnauthorizedHandler
to check out what response it needs. Sadly, the handler does not get the Principal passed in, so it can only return a static exception.
Now, if you wanted to hand-craft this, this would be your entrypoint. Instead of simply using the BasicCredentialAuthFilter
they provide, you would write your own that does the right thing for you.
However, from the code snippet, we can see that all this filter does is to throw a WebApplicationException
. So we can shortcut this.
Our Authenticator
implementation can do the user-locked check beforehand and populate an exception for us to bypass this behaviour. This way, the downstream logic is preserved (react to the WebapplicationException
which I believe is actually a jersey feautere (see: exception mapppers)).
So, consider this example:
public class AuthenticatorTest extends io.dropwizard.Application<Configuration> {
@Override
public void run(Configuration configuration, Environment environment) throws Exception {
environment.jersey().register(new MyHelloResource());
UserAuth a = new UserAuth();
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Principal>()
.setAuthenticator(a).setRealm("SUPER SECRET STUFF").buildAuthFilter()));
}
public static void main(String[] args) throws Exception {
new AuthenticatorTest().run("server", "/home/artur/dev/repo/sandbox/src/main/resources/config/test.yaml");
}
@Path("test")
@Produces(MediaType.APPLICATION_JSON)
public static class MyHelloResource {
@GET
@Path("asd")
@PermitAll
public String test(String x) {
return "Hello";
}
}
public static class UserAuth implements Authenticator<BasicCredentials, Principal> {
@Override
public Optional<Principal> authenticate(BasicCredentials credentials) throws AuthenticationException {
throw new WebApplicationException(Response.status(403).entity("User is blocked").build());
}
}
}
This code simply creates a new exception instead of authenticating the username. This results in this curl:
artur@pandaadb:/$ curl "artur:artur@localhost:9085/api/test/asd"
User is blockedartur@pandaadb:/$ curl "artur:artur@localhost:9085/api/test/asd" -v
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9085 (#0)
* Server auth using Basic with user 'artur'
> GET /api/test/asd HTTP/1.1
> Host: localhost:9085
> Authorization: Basic YXJ0dXI6YXJ0dXI=
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 403 Forbidden
< Date: Thu, 12 Jan 2017 14:19:27 GMT
< Content-Type: application/json
< Content-Length: 15
<
* Connection #0 to host localhost left intact
User is blocked
Now, this is likely not the cleanest solution that you can do. If you need this to be quick, then you can just throw the exception out of the Authenticator
. However, the correct approach would be:
- Implement a new AuthFilter basing it off
io.dropwizard.auth.AuthFilter
- Overwrite the authenticate method on
AuthFilter
to check your user and throw the correct exception there.