I'm trying to understand how authentication and authorization work in DropWizard. I've read their auth guide as well as the dropwizard-security project on GitHub, but feel like I'm still missing a few important concepts.
public class SimpleCredential {
private String password;
public SimpleCredential(String password) {
super();
this.password = password;
}
}
public class SimplePrincipal {
pivate String username;
public SimplePrincipal(String username) {
super();
this.username = username;
}
}
public class SimpleAuthenticator implements Authenticator<SimpleCredential, SimplePrincipal> {
@Override
public Optional<SimplePrincipal> authenticate(SimpleCredential credential) throws AuthenticationException {
if(!"12345".equals(credential.getPassword())) {
throw new AuthenticationException("Sign in failed.");
}
Optional.fromNullable(new SimplePrincipal("simple_user"));
}
}
And then in my Application
subclass:
@Override
public void run(BackendConfiguration configuration, Environment environment) throws Exception {
environment.jersey().register(new BasicAuthProvider<SimplePrincipal>(new SimpleAuthenticator(), "SUPER SECRET STUFF"));
}
And then in a resource method:
@GET
@Path("address/{address_id}")
@Override
public Address getAddress(@Auth @PathParam("address_id") Long id) {
addressDao.getAddressById(id);
}
I think I have this half-configured correctly for basic auth, but not understanding the role that SimpleCredential
and SimplePrincipal
play. Specifically:
SimpleCredential
and SimplePrincipal
play with basic auth? Do I need to add anything to them or other classes to make basic auth work such that the only valid username is simple_user
and the only valid password is 12345
?SimplePrincipal
? Or is the concept of authorization non-existent with web services?Basic Authentication protocol states the client request should have a header in the form of
Authorization: Basic Base64Encoded(username:password)
where Base64Encoded(username:password)
is an actual Base64 encoded string of the username:password
. For example, if my username and password are peeskillet:pass
, the header should be sent out as
Authorization: Basic cGVlc2tpbGxldDpwYXNz
That being said, the Jersey Client (assuming 1.x) has an HTTPBasicAuthFilter
, which is a client side filter, that will handle the encoding part for us. So the client side request might look something like
Client client = Client.create();
WebResource resource = client.resource(BASE_URI);
client.addFilter(new HTTPBasicAuthFilter("peeskillet", "pass"));
String response = resource.get(String.class);
That's all we would need to make a simple GET request with the authorization header.
SimpleCredential: For Basic auth, we would actually be required to use BasicCredentials
, instead of our own credentials. Basically, the request will go through the BasicAuthProvider
. The provider will parse the Authorization header and create a BasicCredentials
object from the parsed username and password. Once that processing has finished, the BasicCredentials
will get passed to our SimpleAuthenticator
's. We use those credentials to authenticate the user.
SimplePrincipal: is basically what we will use to authorize the client. From the authentication process, we can build a principal, that will be used to authorize later (see Question 3). So an example might look something like
import com.google.common.base.Optional;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class SimpleAuthenticator implements Authenticator<BasicCredentials,
SimplePrincipal> {
@Override
public Optional<SimplePrincipal> authenticate(BasicCredentials credentials)
throws AuthenticationException {
// Note: this is horrible authentication. Normally we'd use some
// service to identify the password from the user name.
if (!"pass".equals(credentials.getPassword())) {
throw new AuthenticationException("Boo Hooo!");
}
// from some user service get the roles for this user
// I am explicitly setting it just for simplicity
SimplePrincipal prince = new SimplePrincipal(credentials.getUsername());
prince.getRoles().add(Roles.ADMIN);
return Optional.fromNullable(prince);
}
}
I altered the SimplePrincipal
class a bit, and created a simple Roles
class.
public class SimplePrincipal {
private String username;
private List<String> roles = new ArrayList<>();
public SimplePrincipal(String username) {
this.username = username;
}
public List<String> getRoles() {
return roles;
}
public boolean isUserInRole(String roleToCheck) {
return roles.contains(roleToCheck);
}
public String getUsername() {
return username;
}
}
public class Roles {
public static final String USER = "USER";
public static final String ADMIN = "ADMIN";
public static final String EMPLOYEE = "EMPLOYEE";
}
Some might prefer to have an extra filter layer for authorization, but Dropwizard appears to have the opinionated view that the authorization should occur in the resource class (I forgot exactly where I read it, but I believe their argument is testability). What happens with the SimplePrincial
that we created in the SimpleAuthenticator
is that it can be injected into our resource method, with the use of the @Auth
annotations. We can use the SimplePrincipal
to authorize. Something like
import dropwizard.sample.helloworld.security.Roles;
import dropwizard.sample.helloworld.security.SimplePrincipal;
import io.dropwizard.auth.Auth;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/simple")
public class SimpleResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getResponse(@Auth SimplePrincipal principal) {
if (!principal.isUserInRole(Roles.ADMIN)) {
throw new WebApplicationException(Response.Status.FORBIDDEN);
}
return Response.ok(
"{\"Hello\": \"" + principal.getUsername() + "\"}").build();
}
}
So putting it all together, with this configuration
environment.jersey().register(new BasicAuthProvider<SimplePrincipal>(
new SimpleAuthenticator(),
"Basic Example Realm")
);
and the client credentials I posted previously, when we make the request, we should get a returned
{"Hello": "peeskillet"}
Also it should be mentioned that Basic auth alone is not secure, and it is recommended to be done over SSL
See Related:
A couple things:
For Dropwizard 0.8.x, the configuration of Basic Auth has changed a bit. You can see more here. A simple example would be
SimpleAuthenticator auth = new SimpleAuthenticator();
env.jersey().register(AuthFactory.binder(
new BasicAuthFactory<>(auth,"Example Realm",SimplePrincipal.class)));
See above link for recommended usage of AuthenticationException