Navigation Menu

Skip to content

Commit

Permalink
Merge pull request #1703 from HubSpot/timeouts
Browse files Browse the repository at this point in the history
Better webhook auth timeouts and exception messages
  • Loading branch information
ssalinas committed Feb 7, 2018
2 parents ad879e7 + 051f60a commit 3c357b3
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 45 deletions.
Expand Up @@ -3,11 +3,16 @@
import javax.inject.Inject;

import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;

import io.dropwizard.lifecycle.Managed;

public class SingularityAsyncHttpClient extends AsyncHttpClient implements Managed {

public SingularityAsyncHttpClient(AsyncHttpClientConfig clientConfig) {
super(clientConfig);
}

@Inject
public SingularityAsyncHttpClient() {}

Expand Down
Expand Up @@ -3,16 +3,21 @@
import com.google.inject.Binder;
import com.google.inject.Scopes;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
import com.hubspot.dropwizard.guicier.DropwizardAwareModule;
import com.hubspot.singularity.auth.SingularityAuthFeature;
import com.hubspot.singularity.auth.SingularityAuthenticatorClass;
import com.hubspot.singularity.auth.SingularityAuthorizationHelper;
import com.hubspot.singularity.auth.authenticator.SingularityAuthenticator;
import com.hubspot.singularity.auth.authenticator.SingularityMultiMethodAuthenticator;
import com.hubspot.singularity.auth.datastore.SingularityAuthDatastore;
import com.hubspot.singularity.config.AuthConfiguration;
import com.hubspot.singularity.config.SingularityConfiguration;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;

public class SingularityAuthModule extends DropwizardAwareModule<SingularityConfiguration> {
public static final String WEBHOOK_AUTH_HTTP_CLIENT = "singularity.webhook.auth.http.client";

public SingularityAuthModule() {}

Expand All @@ -21,6 +26,16 @@ public void configure(Binder binder) {
Multibinder<SingularityAuthenticator> multibinder = Multibinder.newSetBinder(binder, SingularityAuthenticator.class);
for (SingularityAuthenticatorClass clazz : getConfiguration().getAuthConfiguration().getAuthenticators()) {
multibinder.addBinding().to(clazz.getAuthenticatorClass());
if (clazz == SingularityAuthenticatorClass.WEBHOOK) {
AuthConfiguration authConfiguration = getConfiguration().getAuthConfiguration();
AsyncHttpClientConfig clientConfig = new AsyncHttpClientConfig.Builder()
.setConnectionTimeoutInMs(authConfiguration.getWebhookAuthConnectTimeoutMs())
.setRequestTimeoutInMs(authConfiguration.getWebhookAuthRequestTimeoutMs())
.setMaxRequestRetry(authConfiguration.getWebhookAuthRetries())
.build();
SingularityAsyncHttpClient webhookAsyncHttpClient = new SingularityAsyncHttpClient(clientConfig);
binder.bind(AsyncHttpClient.class).annotatedWith(Names.named(WEBHOOK_AUTH_HTTP_CLIENT)).toInstance(webhookAsyncHttpClient);
}
}

binder.bind(SingularityAuthFeature.class);
Expand Down
@@ -1,12 +1,16 @@
package com.hubspot.singularity.auth;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.ws.rs.WebApplicationException;

import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Singleton;
import com.hubspot.singularity.SingularityUser;
Expand All @@ -16,6 +20,8 @@

@Singleton
public class SingularityAuthedUserFactory extends AbstractContainerRequestValueFactory<SingularityUser> {
private static final Logger LOG = LoggerFactory.getLogger(SingularityAuthedUserFactory.class);

private final Set<SingularityAuthenticator> authenticators;
private final SingularityConfiguration configuration;

Expand All @@ -27,25 +33,29 @@ public SingularityAuthedUserFactory(Set<SingularityAuthenticator> authenticators

@Override
public SingularityUser provide() {
WebApplicationException unauthenticatedException = null;
List<String> unauthorizedExceptionMessages = new ArrayList<>();
for (SingularityAuthenticator authenticator : authenticators) {
try {
Optional<SingularityUser> maybeUser = authenticator.getUser(getContainerRequest());
if (maybeUser.isPresent()) {
return maybeUser.get();
}
} catch (WebApplicationException e) {
unauthenticatedException = e;
} catch (Throwable t) {
if (t instanceof WebApplicationException) {
WebApplicationException wae = (WebApplicationException) t;
unauthorizedExceptionMessages.add(String.format("%s (%s)", authenticator.getClass().getSimpleName(), wae.getResponse().getEntity().toString()));
} else {
unauthorizedExceptionMessages.add(String.format("%s (%s)", authenticator.getClass().getSimpleName(), t.getMessage()));
}
}
}

// No user found if we got here
if (configuration.getAuthConfiguration().isEnabled()) {
if (unauthenticatedException != null) {
throw unauthenticatedException;
if (!unauthorizedExceptionMessages.isEmpty()) {
throw WebExceptions.unauthorized(String.format("Unable to authenticate using methods: %s", unauthorizedExceptionMessages));
} else {
throw WebExceptions.unauthorized(String.format("Unable to authenticate user using methods: %s",
authenticators.stream().map(SingularityAuthenticator::getClass).collect(Collectors.toList())));
throw WebExceptions.unauthorized(String.format("Unable to authenticate user using methods: %s", authenticators.stream().map(SingularityAuthenticator::getClass).collect(Collectors.toList())));
}
}

Expand Down
@@ -1,5 +1,7 @@
package com.hubspot.singularity.auth.authenticator;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -29,23 +31,28 @@ public SingularityMultiMethodAuthenticator(Set<SingularityAuthenticator> authent
}

public Optional<SingularityUser> authenticate(ContainerRequestContext context) {
WebApplicationException unauthorizedException = null;
List<String> unauthorizedExceptionMessages = new ArrayList<>();
for (SingularityAuthenticator authenticator : authenticators) {
try {
Optional<SingularityUser> maybeUser = authenticator.getUser(context);
if (maybeUser.isPresent()) {
return maybeUser;
}
} catch (WebApplicationException e) {
LOG.trace("Unauthenticated: {}", e.getMessage());
unauthorizedException = e;
} catch (Throwable t) {
LOG.trace("Unauthenticated: {}", t.getMessage());
if (t instanceof WebApplicationException) {
WebApplicationException wae = (WebApplicationException) t;
unauthorizedExceptionMessages.add(String.format("%s (%s)", authenticator.getClass().getSimpleName(), wae.getResponse().getEntity().toString()));
} else {
unauthorizedExceptionMessages.add(String.format("%s (%s)", authenticator.getClass().getSimpleName(), t.getMessage()));
}
}
}

// No user found if we got here
if (configuration.getAuthConfiguration().isEnabled()) {
if (unauthorizedException != null) {
throw unauthorizedException;
if (!unauthorizedExceptionMessages.isEmpty()) {
throw WebExceptions.unauthorized(String.format("Unable to authenticate using methods: %s", unauthorizedExceptionMessages));
} else {
throw WebExceptions.unauthorized(String.format("Unable to authenticate user using methods: %s", authenticators.stream().map(SingularityAuthenticator::getClass).collect(Collectors.toList())));
}
Expand Down
@@ -1,19 +1,25 @@
package com.hubspot.singularity.auth.authenticator;

import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import javax.ws.rs.container.ContainerRequestContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.net.HttpHeaders;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.hubspot.singularity.SingularityAuthModule;
import com.hubspot.singularity.SingularityUser;
import com.hubspot.singularity.WebExceptions;
import com.hubspot.singularity.config.SingularityConfiguration;
Expand All @@ -23,21 +29,40 @@

@Singleton
public class SingularityWebhookAuthenticator implements SingularityAuthenticator {
private static final Logger LOG = LoggerFactory.getLogger(SingularityWebhookAuthenticator.class);

private final AsyncHttpClient asyncHttpClient;
private final WebhookAuthConfiguration webhookAuthConfiguration;
private final ObjectMapper objectMapper;
private final Cache<String, SingularityUserPermissionsResponse> permissionsCache;
private final LoadingCache<String, SingularityUserPermissionsResponse> permissionsCache;

@Inject
public SingularityWebhookAuthenticator(AsyncHttpClient asyncHttpClient,
public SingularityWebhookAuthenticator(@Named(SingularityAuthModule.WEBHOOK_AUTH_HTTP_CLIENT) AsyncHttpClient asyncHttpClient,
SingularityConfiguration configuration,
ObjectMapper objectMapper) {
this.asyncHttpClient = asyncHttpClient;
this.webhookAuthConfiguration = configuration.getWebhookAuthConfiguration();
this.objectMapper = objectMapper;
this.permissionsCache = CacheBuilder.<String, SingularityUserPermissionsResponse>newBuilder()
.expireAfterWrite(webhookAuthConfiguration.getCacheValidationMs(), TimeUnit.MILLISECONDS)
.build();
.refreshAfterWrite(webhookAuthConfiguration.getCacheValidationMs(), TimeUnit.MILLISECONDS)
.build(new CacheLoader<String, SingularityUserPermissionsResponse>() {
@Override
public SingularityUserPermissionsResponse load(String authHeaderValue) throws Exception {
return verifyUncached(authHeaderValue);
}

@Override
public ListenableFuture<SingularityUserPermissionsResponse> reload(String authHeaderVaule, SingularityUserPermissionsResponse oldVaule) {
return ListenableFutureTask.create(() -> {
try {
return verifyUncached(authHeaderVaule);
} catch (Throwable t) {
LOG.warn("Unable to refresh user information", t);
return oldVaule;
}
});
}
});
}

@Override
Expand All @@ -53,39 +78,42 @@ private String extractAuthHeader(ContainerRequestContext context) {
final String authHeaderValue = context.getHeaderString(HttpHeaders.AUTHORIZATION);

if (Strings.isNullOrEmpty(authHeaderValue)) {
throw WebExceptions.unauthorized("(Webhook) No Authorization header present, please log in first");
throw WebExceptions.unauthorized("No Authorization header present, please log in first");
} else {
return authHeaderValue;
}
}

private SingularityUserPermissionsResponse verify(String authHeaderValue) {
SingularityUserPermissionsResponse maybeCachedPermissions = permissionsCache.getIfPresent(authHeaderValue);
if (maybeCachedPermissions != null) {
return maybeCachedPermissions;
} else {
try {
Response response = asyncHttpClient.prepareGet(webhookAuthConfiguration.getAuthVerificationUrl())
.addHeader("Authorization", authHeaderValue)
.execute()
.get();
if (response.getStatusCode() > 299) {
throw WebExceptions.unauthorized(String.format("(Webhook) Got status code %d when verifying jwt", response.getStatusCode()));
} else {
String responseBody = response.getResponseBody();
SingularityUserPermissionsResponse permissionsResponse = objectMapper.readValue(responseBody, SingularityUserPermissionsResponse.class);
if (!permissionsResponse.getUser().isPresent()) {
throw WebExceptions.unauthorized(String.format("(Webhook) No user present in response %s", permissionsResponse));
}
if (!permissionsResponse.getUser().get().isAuthenticated()) {
throw WebExceptions.unauthorized(String.format("(Webhook) User not authenticated (response: %s)", permissionsResponse));
}
permissionsCache.put(authHeaderValue, permissionsResponse);
return permissionsResponse;
try {
return permissionsCache.get(authHeaderValue);
} catch (Throwable t) {
throw WebExceptions.unauthorized(String.format("Exception while verifying token: %s", t.getMessage()));
}
}

private SingularityUserPermissionsResponse verifyUncached(String authHeaderValue) {
try {
Response response = asyncHttpClient.prepareGet(webhookAuthConfiguration.getAuthVerificationUrl())
.addHeader("Authorization", authHeaderValue)
.execute()
.get();
if (response.getStatusCode() > 299) {
throw WebExceptions.unauthorized(String.format("Got status code %d when verifying jwt", response.getStatusCode()));
} else {
String responseBody = response.getResponseBody();
SingularityUserPermissionsResponse permissionsResponse = objectMapper.readValue(responseBody, SingularityUserPermissionsResponse.class);
if (!permissionsResponse.getUser().isPresent()) {
throw WebExceptions.unauthorized(String.format("No user present in response %s", permissionsResponse));
}
if (!permissionsResponse.getUser().get().isAuthenticated()) {
throw WebExceptions.unauthorized(String.format("User not authenticated (response: %s)", permissionsResponse));
}
} catch (IOException|ExecutionException|InterruptedException e) {
throw WebExceptions.unauthorized(String.format("(Webhook) Exception while verifying token: %s", e.getMessage()));
permissionsCache.put(authHeaderValue, permissionsResponse);
return permissionsResponse;
}
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
}
Expand Up @@ -52,6 +52,15 @@ public class AuthConfiguration {
@NotNull
private String requestUserHeaderName = "X-Username"; // used by SingularityHeaderPassthroughAuthenticator

@JsonProperty
private int webhookAuthRequestTimeoutMs = 2000;

@JsonProperty
private int webhookAuthRetries = 2;

@JsonProperty
private int webhookAuthConnectTimeoutMs = 2000;

public boolean isEnabled() {
return enabled;
}
Expand Down Expand Up @@ -133,4 +142,31 @@ public String getRequestUserHeaderName() {
public void setRequestUserHeaderName(String requestUserHeaderName) {
this.requestUserHeaderName = requestUserHeaderName;
}

public int getWebhookAuthRequestTimeoutMs() {
return webhookAuthRequestTimeoutMs;
}

public AuthConfiguration setWebhookAuthRequestTimeoutMs(int webhookAuthRequestTimeoutMs) {
this.webhookAuthRequestTimeoutMs = webhookAuthRequestTimeoutMs;
return this;
}

public int getWebhookAuthRetries() {
return webhookAuthRetries;
}

public AuthConfiguration setWebhookAuthRetries(int webhookAuthRetries) {
this.webhookAuthRetries = webhookAuthRetries;
return this;
}

public int getWebhookAuthConnectTimeoutMs() {
return webhookAuthConnectTimeoutMs;
}

public AuthConfiguration setWebhookAuthConnectTimeoutMs(int webhookAuthConnectTimeoutMs) {
this.webhookAuthConnectTimeoutMs = webhookAuthConnectTimeoutMs;
return this;
}
}

0 comments on commit 3c357b3

Please sign in to comment.