Skip to content

Commit

Permalink
[DUOS-2873][risk=no] Cache request headers and populate auth user wit…
Browse files Browse the repository at this point in the history
…h them (#2233)
  • Loading branch information
rushtong authored Feb 6, 2024
1 parent f67f04f commit 4c307b4
Show file tree
Hide file tree
Showing 15 changed files with 191 additions and 285 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.broadinstitute.consent.http.cloudstore.GCSService;
import org.broadinstitute.consent.http.configurations.ConsentConfiguration;
import org.broadinstitute.consent.http.db.UserRoleDAO;
import org.broadinstitute.consent.http.filters.RequestHeaderCacheFilter;
import org.broadinstitute.consent.http.filters.ResponseServerFilter;
import org.broadinstitute.consent.http.health.ElasticSearchHealthCheck;
import org.broadinstitute.consent.http.health.GCSHealthCheck;
Expand Down Expand Up @@ -262,6 +263,7 @@ public void run(ConsentConfiguration config, Environment env) {
List<AuthFilter> filters = List.of(
defaultAuthFilter,
new OAuthCustomAuthFilter(authenticator, userRoleDAO));
env.jersey().register(RequestHeaderCacheFilter.class);
env.jersey().register(new AuthDynamicFeature(new ChainedAuthFilter(filters)));
env.jersey().register(RolesAllowedDynamicFeature.class);
env.jersey().register(new AuthValueFactoryProvider.Binder<>(AuthUser.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ OntologyService providesOntologyService() {

@Provides
OAuthAuthenticator providesOAuthAuthenticator() {
return new OAuthAuthenticator(providesClient(), providesSamService());
return new OAuthAuthenticator(providesSamService());
}

@Provides
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import com.google.inject.Inject;
import io.dropwizard.auth.Authenticator;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ServerErrorException;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.broadinstitute.consent.http.filters.ClaimsCache;
import org.broadinstitute.consent.http.models.AuthUser;
import org.broadinstitute.consent.http.models.sam.UserStatus;
import org.broadinstitute.consent.http.models.sam.UserStatusInfo;
Expand All @@ -18,31 +18,48 @@

public class OAuthAuthenticator implements Authenticator<String, AuthUser>, ConsentLogger {

private static final String USER_INFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo?access_token=";
private final Client client;
private final SamService samService;
private final ClaimsCache claimsCache;

@Inject
public OAuthAuthenticator(Client client, SamService samService) {
this.client = client;
public OAuthAuthenticator(SamService samService) {
this.samService = samService;
this.claimsCache = ClaimsCache.getInstance();
}

@Override
public Optional<AuthUser> authenticate(String bearer) {
try {
GenericUser genericUser = getUserProfileInfo(bearer);
AuthUser user = Objects.nonNull(genericUser) ?
new AuthUser(genericUser).setAuthToken(bearer) :
new AuthUser().setAuthToken(bearer);
AuthUser userWithStatus = getUserWithStatusInfo(user);
return Optional.of(userWithStatus);
var headers = claimsCache.cache.getIfPresent(bearer);
if (headers != null) {
AuthUser user = buildAuthUserFromHeaders(headers);
AuthUser userWithStatus = getUserWithStatusInfo(user);
return Optional.of(userWithStatus);
}
logException(new ServerErrorException("Error reading request headers", 500));
return Optional.empty();
} catch (Exception e) {
logException("Error authenticating credentials", e);
return Optional.empty();
}
}

private AuthUser buildAuthUserFromHeaders(Map<String, String> headers) {
String aud = headers.get(ClaimsCache.OAUTH2_CLAIM_aud);
String token = headers.get(ClaimsCache.OAUTH2_CLAIM_access_token);
String email = headers.get(ClaimsCache.OAUTH2_CLAIM_email);
String name = headers.get(ClaimsCache.OAUTH2_CLAIM_name);
// Name is not a guaranteed header
if (name == null) {
name = email;
}
return new AuthUser()
.setAud(aud)
.setAuthToken(token)
.setEmail(email)
.setName(name);
}

/**
* Attempt to get the registration status of the current user and set the value on AuthUser
*
Expand Down Expand Up @@ -84,26 +101,4 @@ private AuthUser getUserWithStatusInfo(AuthUser authUser) {
return authUser;
}

/**
* This method is currently google-centric. When we fully support B2C authentication, we should
* ensure that we can look up user info from a MS service.
*
* @param bearer Bearer Token
* @return GenericUser
*/
private GenericUser getUserProfileInfo(String bearer) {
GenericUser u = null;
try {
Response response = this.client.
target(USER_INFO_URL + bearer).
request(MediaType.APPLICATION_JSON_TYPE).
get(Response.class);
String result = response.readEntity(String.class);
u = new GenericUser(result);
} catch (Exception e) {
logWarn("Error getting Google user info from token: " + e.getMessage());
}
return u;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.broadinstitute.consent.http.filters;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
* Manage a cache of bearer token to map of `OAUTH2_CLAIM` headers for every request. This is
* useful in cases where components need, but do not have access to, the full request context.
*/
public class ClaimsCache {

private static ClaimsCache INSTANCE;
public final Cache<String, Map<String, String>> cache;
public final static String OAUTH2_CLAIM_email = "OAUTH2_CLAIM_email";
public final static String OAUTH2_CLAIM_name = "OAUTH2_CLAIM_name";
public final static String OAUTH2_CLAIM_access_token = "OAUTH2_CLAIM_access_token";
public final static String OAUTH2_CLAIM_aud = "OAUTH2_CLAIM_aud";

private ClaimsCache() {
cache = CacheBuilder
.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}

public static ClaimsCache getInstance() {
if (INSTANCE == null) {
INSTANCE = new ClaimsCache();
}
return INSTANCE;
}

private String getFirst(List<String> headerValues) {
if (headerValues == null) {
return null;
}
return headerValues.stream().findFirst().orElse(null);
}

public void loadCache(String token, MultivaluedMap<String, String> headers) {
if (this.cache.getIfPresent(token) == null) {
Map<String, String> claimsMap = headers.entrySet()
.stream()
.filter(e -> e.getKey().startsWith("OAUTH2_CLAIM"))
.map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), getFirst(e.getValue())))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
this.cache.put(token, claimsMap);
this.cache.cleanUp();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.broadinstitute.consent.http.filters;

import jakarta.annotation.Priority;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;

@Provider
@Priority(Integer.MIN_VALUE)
public class RequestHeaderCacheFilter implements ContainerRequestFilter {

private final ClaimsCache claimsCache = ClaimsCache.getInstance();

@Override
public void filter(ContainerRequestContext containerRequestContext) throws IOException {
var headers = containerRequestContext.getHeaders();
var token = headers.getFirst(HttpHeaders.AUTHORIZATION);
if (token != null) {
var bearer = token.replace("Bearer ", "");
claimsCache.loadCache(bearer, headers);
}
}
}
Loading

0 comments on commit 4c307b4

Please sign in to comment.