DropWizard Auth by Example

IAmYourFaja picture IAmYourFaja · Dec 10, 2014 · Viewed 19.5k times · Source

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:

  1. How do I set basic auth username/password from the Jersey/JAX-RS client?
  2. What role do 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?
  3. How do I enforce access/authroization/roles via SimplePrincipal? Or is the concept of authorization non-existent with web services?

Answer

Paul Samsotha picture Paul Samsotha · Dec 10, 2014

Question 1:

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.

Question 2:

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";
}

Question 3:

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:


UPDATE

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