Add support for optional authentication

This commit is contained in:
Moxie Marlinspike 2018-05-04 03:51:03 -07:00
parent ec37a3c67a
commit 80ac3cf7e6
9 changed files with 163 additions and 33 deletions

View File

@ -79,8 +79,8 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>

View File

@ -6,6 +6,9 @@ import javax.ws.rs.container.DynamicFeature;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.FeatureContext;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Optional;
import io.dropwizard.auth.Auth;
@ -19,22 +22,30 @@ public class AuthDynamicFeature implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
AnnotatedMethod annotatedMethod = new AnnotatedMethod(resourceInfo.getResourceMethod());
Annotation[][] parameterAnnotations = annotatedMethod.getParameterAnnotations();
Class<?>[] parameterTypes = annotatedMethod.getParameterTypes();
AnnotatedMethod annotatedMethod = new AnnotatedMethod(resourceInfo.getResourceMethod());
Annotation[][] parameterAnnotations = annotatedMethod.getParameterAnnotations();
Class<?>[] parameterTypes = annotatedMethod.getParameterTypes ();
Type[] parameterGenericTypes = annotatedMethod.getGenericParameterTypes();
verifyAuthAnnotations(parameterAnnotations);
for (int i=0;i<parameterAnnotations.length;i++) {
for (Annotation annotation : parameterAnnotations[i]) {
if (annotation instanceof Auth) {
context.register(getFilterFor(parameterTypes[i]));
Type parameterType = parameterTypes[i];
if (parameterType == Optional.class) {
parameterType = ((ParameterizedType)parameterGenericTypes[i]).getActualTypeArguments()[0];
context.register(new WebApplicationExceptionCatchingFilter(getFilterFor(parameterType)));
} else {
context.register(getFilterFor(parameterType));
}
}
}
}
}
private AuthFilter getFilterFor(Class<?> parameterType) {
private AuthFilter getFilterFor(Type parameterType) {
for (AuthFilter filter : authFilters) {
if (filter.supports(parameterType)) return filter;
}

View File

@ -6,6 +6,9 @@ import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestFilter;
import java.lang.reflect.Type;
import java.util.Optional;
import io.dropwizard.auth.DefaultUnauthorizedHandler;
import io.dropwizard.auth.UnauthorizedHandler;
@ -19,7 +22,7 @@ public abstract class AuthFilter<C, P> implements ContainerRequestFilter {
protected UnauthorizedHandler unauthorizedHandler = new DefaultUnauthorizedHandler();
public boolean supports(Class<?> clazz) {
public boolean supports(Type clazz) {
return clazz.equals(principalType);
}

View File

@ -14,6 +14,7 @@ import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.security.Principal;
import java.util.Optional;
import io.dropwizard.auth.Auth;
@ -33,29 +34,62 @@ public class AuthValueFactoryProvider extends AbstractValueFactoryProvider {
return null;
}
return new AbstractContainerRequestValueFactory() {
if (parameter.getRawType() == Optional.class) {
return new OptionalContainerRequestValueFactory(parameter);
} else {
return new StandardContainerReqeustValueFactory(parameter);
}
}
/**
* @return {@link Principal} stored on the request, or {@code null} if no object was found.
*/
public Object provide() {
Principal principal = getContainerRequest().getSecurityContext().getUserPrincipal();
private static class StandardContainerReqeustValueFactory extends AbstractContainerRequestValueFactory {
private final Parameter parameter;
if (principal == null) {
throw new IllegalStateException("Cannot inject a custom principal into unauthenticated request");
}
StandardContainerReqeustValueFactory(Parameter parameter) {
this.parameter = parameter;
}
if (!(principal instanceof AuthPrincipal)) {
throw new IllegalArgumentException("Cannot inject a non-AuthPrincipal into request");
}
/**
* @return {@link Principal} stored on the request, or {@code null} if no object was found.
*/
public Object provide() {
Principal principal = getContainerRequest().getSecurityContext().getUserPrincipal();
if (!parameter.getRawType().isAssignableFrom(((AuthPrincipal)principal).getAuthenticated().getClass())) {
throw new IllegalArgumentException("Authenticated principal is of the wrong type!");
}
return parameter.getRawType().cast(((AuthPrincipal) principal).getAuthenticated());
if (principal == null) {
throw new IllegalStateException("Cannot inject a custom principal into unauthenticated request");
}
};
if (!(principal instanceof AuthPrincipal)) {
throw new IllegalArgumentException("Cannot inject a non-AuthPrincipal into request");
}
if (!parameter.getRawType().isAssignableFrom(((AuthPrincipal)principal).getAuthenticated().getClass())) {
throw new IllegalArgumentException("Authenticated principal is of the wrong type!");
}
return parameter.getRawType().cast(((AuthPrincipal) principal).getAuthenticated());
}
}
private static class OptionalContainerRequestValueFactory extends AbstractContainerRequestValueFactory {
private final Parameter parameter;
OptionalContainerRequestValueFactory(Parameter parameter) {
this.parameter = parameter;
}
/**
* @return {@link Principal} stored on the request, or {@code null} if no object was found.
*/
public Object provide() {
Principal principal = getContainerRequest().getSecurityContext().getUserPrincipal();
if (principal != null && !(principal instanceof AuthPrincipal)) {
throw new IllegalArgumentException("Cannot inject a non-AuthPrincipal into request");
}
if (principal == null) return Optional.empty();
else return Optional.of(((AuthPrincipal)principal).getAuthenticated());
}
}
@Singleton

View File

@ -1,7 +1,7 @@
package org.whispersystems.dropwizard.simpleauth;
import com.google.common.base.Optional;
import java.util.Optional;
import io.dropwizard.auth.AuthenticationException;
@ -17,7 +17,7 @@ public interface Authenticator<C, P> {
* Given a set of user-provided credentials, return an optional principal.
*
* If the credentials are valid and map to a principal, returns an {@code Optional.of(p)}.
*
*
* If the credentials are invalid, returns an {@code Optional.absent()}.
*
* @param credentials a set of user-provided credentials

View File

@ -1,6 +1,5 @@
package org.whispersystems.dropwizard.simpleauth;
import com.google.common.base.Optional;
import com.google.common.io.BaseEncoding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -13,6 +12,7 @@ import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.HttpHeaders;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.basic.BasicCredentials;

View File

@ -0,0 +1,41 @@
package org.whispersystems.dropwizard.simpleauth;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.io.IOException;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
/**
* A {@link ContainerRequestFilter} decorator which catches any {@link
* WebApplicationException WebApplicationExceptions} thrown by an
* underlying {@code ContextRequestFilter}.
*/
@Priority(Priorities.AUTHENTICATION)
class WebApplicationExceptionCatchingFilter implements ContainerRequestFilter {
private final ContainerRequestFilter underlying;
public WebApplicationExceptionCatchingFilter(ContainerRequestFilter underlying) {
Preconditions.checkNotNull(underlying, "Underlying ContainerRequestFilter is not set");
this.underlying = underlying;
}
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
try {
underlying.filter(requestContext);
} catch (WebApplicationException err) {
// Pass through.
}
}
@VisibleForTesting
ContainerRequestFilter getUnderlying() {
return underlying;
}
}

View File

@ -1,15 +1,20 @@
package org.whispersystems.dropwizard.simpleauth;
import com.google.common.base.Optional;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.FeatureContext;
import java.util.Optional;
import io.dropwizard.auth.Auth;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.basic.BasicCredentials;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
import static org.mockito.Mockito.*;
public class AuthDynamicFeatureTest {
@ -34,11 +39,33 @@ public class AuthDynamicFeatureTest {
dynamicFeature.configure(resourceInfo, featureContext);
verify(featureContext).register(eq(stringPrincipal));
reset(featureContext);
when(resourceInfo.getResourceMethod()).thenReturn(MockMethod.class.getDeclaredMethod("integerAuthParam", Integer.class));
dynamicFeature.configure(resourceInfo, featureContext);
verify(featureContext).register(eq(integerPrincipal));
reset(featureContext);
when(resourceInfo.getResourceMethod()).thenReturn(MockMethod.class.getDeclaredMethod("optionalStringAuthParam", Optional.of("test").getClass()));
dynamicFeature.configure(resourceInfo, featureContext);
ArgumentCaptor<ContainerRequestFilter> stringOptionalCaptor = ArgumentCaptor.forClass(ContainerRequestFilter.class);
verify(featureContext).register(stringOptionalCaptor.capture());
assertTrue(stringOptionalCaptor.getValue() instanceof WebApplicationExceptionCatchingFilter);
assertEquals(((WebApplicationExceptionCatchingFilter)stringOptionalCaptor.getValue()).getUnderlying(), stringPrincipal);
reset(featureContext);
when(resourceInfo.getResourceMethod()).thenReturn(MockMethod.class.getDeclaredMethod("optionalIntegerAuthParam", Optional.of(1).getClass()));
dynamicFeature.configure(resourceInfo, featureContext);
ArgumentCaptor<ContainerRequestFilter> integerOptionalCaptor = ArgumentCaptor.forClass(ContainerRequestFilter.class);
verify(featureContext, times(1)).register(integerOptionalCaptor.capture());
assertTrue(integerOptionalCaptor.getValue() instanceof WebApplicationExceptionCatchingFilter);
assertEquals(((WebApplicationExceptionCatchingFilter)integerOptionalCaptor.getValue()).getUnderlying(), integerPrincipal);
}
@Test
@ -55,12 +82,26 @@ public class AuthDynamicFeatureTest {
} catch (Exception e) {
// Good
}
when(resourceInfo.getResourceMethod()).thenReturn(MockMethod.class.getDeclaredMethod("multipleOptionalAuthParams", Optional.of("foo").getClass(), Optional.of("bar").getClass()));
try {
dynamicFeature.configure(resourceInfo, featureContext);
throw new AssertionError("Shouldn't support multiple auth tags!");
} catch (Exception e) {
// Good
}
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static class MockMethod {
public void multipleAuthParams(@Auth String foo, @Auth String bar) {}
public void stringAuthParam(@Auth String foo) {}
public void integerAuthParam(@Auth Integer bar) {}
public void optionalStringAuthParam(@Auth Optional<String> foo) {}
public void optionalIntegerAuthParam(@Auth Optional<Integer> bar) {}
public void multipleOptionalAuthParams(@Auth Optional<String> foo, @Auth Optional<String> bar) {}
}
private static class StringAuthenticator implements Authenticator<BasicCredentials, String> {
@ -72,7 +113,7 @@ public class AuthDynamicFeatureTest {
credentials.getPassword().equals("password"))
return Optional.of("user");
return Optional.absent();
return Optional.empty();
}
}

View File

@ -1,6 +1,5 @@
package org.whispersystems.dropwizard.simpleauth;
import com.google.common.base.Optional;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@ -12,6 +11,7 @@ import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.SecurityContext;
import java.io.IOException;
import java.util.Optional;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.basic.BasicCredentials;
@ -76,7 +76,7 @@ public class BasicCredentialAuthFilterTest {
return Optional.of("user");
}
return Optional.absent();
return Optional.empty();
}
}
}