How can I test HMAC authentication using Dropwizard?

后端 未结 1 1734
伪装坚强ぢ
伪装坚强ぢ 2021-01-13 13:31

I\'m just getting started with Dropwizard 0.4.0, and I would like some help with HMAC authentication. Has anybody got any advice?

Thank you in advance.

相关标签:
1条回答
  • 2021-01-13 14:23

    At present Dropwizard doesn't support HMAC authentication right out of the box, so you'd have to write your own authenticator. A typical choice for HMAC authentication is to use the HTTP Authorization header. The following code expects this header in the following format:

    Authorization: <algorithm> <apiKey> <digest>
    

    An example would be

    Authorization: HmacSHA1 abcd-efgh-1234 sdafkljlkansdaflk2354jlkj5345345dflkmsdf
    

    The digest is built from the content of the body (marshalled entity) prior to URL encoding with the HMAC shared secret appended as base64. For a non-body request, such as GET or HEAD, the content is taken as the complete URI path and parameters with the secret key appended.

    To implement this in a way that Dropwizard can work with it requires you to copy the BasicAuthenticator code present in the dropwizard-auth module into your own code and modify it with something like this:

    import com.google.common.base.Optional;
    import com.sun.jersey.api.core.HttpContext;
    import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
    import com.yammer.dropwizard.auth.AuthenticationException;
    import com.yammer.dropwizard.auth.Authenticator;
    
    import javax.ws.rs.WebApplicationException;
    import javax.ws.rs.core.HttpHeaders;
    import javax.ws.rs.core.MediaType;
    import javax.ws.rs.core.Response;
    
    class HmacAuthInjectable<T> extends AbstractHttpContextInjectable<T> {
      private static final String PREFIX = "HmacSHA1";
      private static final String HEADER_VALUE = PREFIX + " realm=\"%s\"";
    
      private final Authenticator<HmacCredentials, T> authenticator;
      private final String realm;
      private final boolean required;
    
      HmacAuthInjectable(Authenticator<HmacCredentials, T> authenticator, String realm, boolean required) {
        this.authenticator = authenticator;
        this.realm = realm;
        this.required = required;
      }
    
      public Authenticator<HmacCredentials, T> getAuthenticator() {
        return authenticator;
      }
    
      public String getRealm() {
        return realm;
      }
    
      public boolean isRequired() {
        return required;
      }
    
      @Override
      public T getValue(HttpContext c) {
    
        try {
          final String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION);
          if (header != null) {
    
            final String[] authTokens = header.split(" ");
    
            if (authTokens.length != 3) {
              // Malformed
              HmacAuthProvider.LOG.debug("Error decoding credentials (length is {})", authTokens.length);
              throw new WebApplicationException(Response.Status.BAD_REQUEST);
            }
    
            final String algorithm = authTokens[0];
            final String apiKey = authTokens[1];
            final String signature = authTokens[2];
            final String contents;
    
            // Determine which part of the request will be used for the content
            final String method = c.getRequest().getMethod().toUpperCase();
            if ("GET".equals(method) ||
              "HEAD".equals(method) ||
              "DELETE".equals(method)) {
              // No entity so use the URI
              contents = c.getRequest().getRequestUri().toString();
            } else {
              // Potentially have an entity (even in OPTIONS) so use that
              contents = c.getRequest().getEntity(String.class);
            }
    
            final HmacCredentials credentials = new HmacCredentials(algorithm, apiKey, signature, contents);
    
            final Optional<T> result = authenticator.authenticate(credentials);
            if (result.isPresent()) {
              return result.get();
            }
          }
        } catch (IllegalArgumentException e) {
          HmacAuthProvider.LOG.debug(e, "Error decoding credentials");
        } catch (AuthenticationException e) {
          HmacAuthProvider.LOG.warn(e, "Error authenticating credentials");
          throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
        }
    
        if (required) {
          throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
            .header(HttpHeaders.AUTHORIZATION,
              String.format(HEADER_VALUE, realm))
            .entity("Credentials are required to access this resource.")
            .type(MediaType.TEXT_PLAIN_TYPE)
            .build());
        }
        return null;
      }
    }
    

    The above is not perfect, but it'll get you started. You may want to refer to the MultiBit Merchant release candidate source code (MIT license) for a more up to date version and the various supporting classes.

    The next step is to integrate the authentication process into your ResourceTest subclass. Unfortunately, Dropwizard doesn't provide a good entry point for authentication providers in v0.4.0, so you may want to introduce your own base class, similar to this:

    import com.google.common.collect.Lists;
    import com.google.common.collect.Sets;
    import com.sun.jersey.api.client.Client;
    import com.sun.jersey.test.framework.AppDescriptor;
    import com.sun.jersey.test.framework.JerseyTest;
    import com.sun.jersey.test.framework.LowLevelAppDescriptor;
    import com.xeiam.xchange.utils.CryptoUtils;
    import com.yammer.dropwizard.bundles.JavaBundle;
    import com.yammer.dropwizard.jersey.DropwizardResourceConfig;
    import com.yammer.dropwizard.jersey.JacksonMessageBodyProvider;
    import com.yammer.dropwizard.json.Json;
    import org.codehaus.jackson.map.Module;
    import org.junit.After;
    import org.junit.Before;
    import org.multibit.mbm.auth.hmac.HmacAuthProvider;
    import org.multibit.mbm.auth.hmac.HmacAuthenticator;
    import org.multibit.mbm.persistence.dao.UserDao;
    import org.multibit.mbm.persistence.dto.User;
    import org.multibit.mbm.persistence.dto.UserBuilder;
    
    import java.io.UnsupportedEncodingException;
    import java.security.GeneralSecurityException;
    import java.util.List;
    import java.util.Set;
    
    import static org.mockito.Mockito.mock;
    import static org.mockito.Mockito.when;
    
    /**
    * A base test class for testing Dropwizard resources.
    */
    public abstract class BaseResourceTest {
      private final Set<Object> singletons = Sets.newHashSet();
      private final Set<Object> providers = Sets.newHashSet();
      private final List<Module> modules = Lists.newArrayList();
    
      private JerseyTest test;
    
      protected abstract void setUpResources() throws Exception;
    
      protected void addResource(Object resource) {
        singletons.add(resource);
      }
    
      public void addProvider(Object provider) {
        providers.add(provider);
      }
    
      protected void addJacksonModule(Module module) {
        modules.add(module);
      }
    
      protected Json getJson() {
        return new Json();
      }
    
      protected Client client() {
        return test.client();
      }
    
      @Before
      public void setUpJersey() throws Exception {
        setUpResources();
        this.test = new JerseyTest() {
          @Override
          protected AppDescriptor configure() {
            final DropwizardResourceConfig config = new DropwizardResourceConfig();
            for (Object provider : JavaBundle.DEFAULT_PROVIDERS) { // sorry, Scala folks
              config.getSingletons().add(provider);
            }
            for (Object provider : providers) {
              config.getSingletons().add(provider);
            }
            Json json = getJson();
            for (Module module : modules) {
              json.registerModule(module);
            }
            config.getSingletons().add(new JacksonMessageBodyProvider(json));
            config.getSingletons().addAll(singletons);
            return new LowLevelAppDescriptor.Builder(config).build();
          }
        };
        test.setUp();
      }
    
      @After
      public void tearDownJersey() throws Exception {
        if (test != null) {
          test.tearDown();
        }
      }
    
      /**
    * @param contents The content to sign with the default HMAC process (POST body, GET resource path)
    * @return
    */
      protected String buildHmacAuthorization(String contents, String apiKey, String secretKey) throws UnsupportedEncodingException, GeneralSecurityException {
        return String.format("HmacSHA1 %s %s",apiKey, CryptoUtils.computeSignature("HmacSHA1", contents, secretKey));
      }
    
      protected void setUpAuthenticator() {
        User user = UserBuilder
          .getInstance()
          .setUUID("abc123")
          .setSecretKey("def456")
          .build();
    
        //
        UserDao userDao = mock(UserDao.class);
        when(userDao.getUserByUUID("abc123")).thenReturn(user);
    
        HmacAuthenticator authenticator = new HmacAuthenticator();
        authenticator.setUserDao(userDao);
    
        addProvider(new HmacAuthProvider<User>(authenticator, "REST"));
      }
    }
    

    Again, the above code is not perfect, but the idea is to allow a mocked up UserDao to provide a standard user with a known shared secret key. You'd have to introduce your own UserBuilder implementation for testing purposes.

    Finally, with the above code a Dropwizard Resource that had an endpoint like this:

    import com.google.common.base.Optional;
    import com.yammer.dropwizard.auth.Auth;
    import com.yammer.metrics.annotation.Timed;
    import org.multibit.mbm.core.Saying;
    import org.multibit.mbm.persistence.dto.User;
    
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.Produces;
    import javax.ws.rs.QueryParam;
    import javax.ws.rs.core.MediaType;
    import java.util.concurrent.atomic.AtomicLong;
    
    @Path("/")
    @Produces(MediaType.APPLICATION_JSON)
    public class HelloWorldResource {
      private final String template;
      private final String defaultName;
      private final AtomicLong counter;
    
      public HelloWorldResource(String template, String defaultName) {
        this.template = template;
        this.defaultName = defaultName;
        this.counter = new AtomicLong();
      }
    
      @GET
      @Timed
      @Path("/hello-world")
      public Saying sayHello(@QueryParam("name") Optional<String> name) {
        return new Saying(counter.incrementAndGet(),
          String.format(template, name.or(defaultName)));
      }
    
      @GET
      @Timed
      @Path("/secret")
      public Saying saySecuredHello(@Auth User user) {
        return new Saying(counter.incrementAndGet(),
          "You cracked the code!");
      }
    
    }
    

    could be tested with a unit test that was configured like this:

    import org.junit.Test;
    import org.multibit.mbm.core.Saying;
    import org.multibit.mbm.test.BaseResourceTest;
    
    import javax.ws.rs.core.HttpHeaders;
    
    import static org.junit.Assert.assertEquals;
    
    public class HelloWorldResourceTest extends BaseResourceTest {
    
    
      @Override
      protected void setUpResources() {
        addResource(new HelloWorldResource("Hello, %s!","Stranger"));
    
        setUpAuthenticator();
      }
    
      @Test
      public void simpleResourceTest() throws Exception {
    
        Saying expectedSaying = new Saying(1,"Hello, Stranger!");
    
        Saying actualSaying = client()
          .resource("/hello-world")
          .get(Saying.class);
    
        assertEquals("GET hello-world returns a default",expectedSaying.getContent(),actualSaying.getContent());
    
      }
    
    
      @Test
      public void hmacResourceTest() throws Exception {
    
        String authorization = buildHmacAuthorization("/secret", "abc123", "def456");
    
        Saying actual = client()
          .resource("/secret")
          .header(HttpHeaders.AUTHORIZATION, authorization)
          .get(Saying.class);
    
        assertEquals("GET secret returns unauthorized","You cracked the code!", actual.getContent());
    
      }
    
    
    }
    

    Hope this helps you get started.

    0 讨论(0)
提交回复
热议问题