Skip to content

Commit

Permalink
[MCC-785068] Use Cache Control header as TTL (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
hballangan-mdsol authored Jul 17, 2021
1 parent 74f3cd1 commit a71a7b8
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 23 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- parsing code to test with mauth-protocol-test-suite.
- Caffeine dependency for local cache

### Changed
- Dependency update
- Update sttp to 3.x from 2.x. Note this is a new major sttp version with new package prefix `sttp.client3`
- Remove silencer as 2.12.13 also got configurable warnings now

- Use Caffeine as local cache instead of Guava Cache
- Added Cache-Control header in FakeMauthServer success response
- Use Cache-Control max-age header as ttl in public key local cache and only set configuration value as fallback

## Removed
- Removed mauth-proxy. The library we depend on (littleproxy) has been unmaintained for a long time
and there are better mauth proxy alternatives like https://github.com/mdsol/go-mauth-proxy
Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ lazy val `mauth-signer-apachehttp` = javaModuleProject("mauth-signer-apachehttp"
publishSettings,
libraryDependencies ++=
Dependencies.compile(apacheHttpClient).map(withExclusions) ++
Dependencies.compile(caffeine).map(withExclusions) ++
Dependencies.test(scalaMock, scalaTest).map(withExclusions)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package com.mdsol.mauth.apache;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.mdsol.mauth.AuthenticatorConfiguration;
import com.mdsol.mauth.Signer;
import com.mdsol.mauth.exception.HttpClientPublicKeyProviderException;
import com.mdsol.mauth.util.MAuthKeysHelper;
import com.mdsol.mauth.utils.ClientPublicKeyProvider;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.*;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
Expand All @@ -24,8 +20,10 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

Expand All @@ -39,28 +37,23 @@ public class HttpClientPublicKeyProvider implements ClientPublicKeyProvider {
private final PublicKeyResponseHandler publicKeyResponseHandler;

private LoadingCache<UUID, PublicKey> publicKeyCache;
private Long ttl;

public HttpClientPublicKeyProvider(AuthenticatorConfiguration configuration, Signer signer) {
this.configuration = configuration;
this.signer = signer;
this.httpclient = HttpClients.createDefault();
this.publicKeyResponseHandler = new PublicKeyResponseHandler();
setupCache(configuration.getTimeToLive());
}

private void setupCache(long timeToLiveInSeconds) {
private void setupCache() {
publicKeyCache =
CacheBuilder.newBuilder()
.expireAfterAccess(timeToLiveInSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<UUID, PublicKey>() {
@Override
public PublicKey load(UUID appUUID) throws Exception {
return getPublicKeyFromEureka(appUUID);
}
});
Caffeine.newBuilder()
.expireAfterAccess(ttl, TimeUnit.SECONDS)
.build(this::getPublicKeyFromMauth);
}

private PublicKey getPublicKeyFromEureka(UUID appUUID) {
private PublicKey getPublicKeyFromMauth(UUID appUUID) {
byte[] payload = new byte[0];
String requestUrlPath = getRequestUrlPath(appUUID);
Map<String, String> headers = signer.generateRequestHeaders("GET", requestUrlPath, payload, "");
Expand All @@ -72,9 +65,16 @@ private PublicKey getPublicKeyFromEureka(UUID appUUID) {
@Override
public PublicKey getPublicKey(UUID appUUID) {
try {
if (publicKeyCache == null) {
// Lazy load public key cache so that we can set the ttl based on the first response max-age
// Do Eureka call first to set the ttl
PublicKey key = getPublicKeyFromMauth(appUUID);
setupCache();
publicKeyCache.put(appUUID, key);
}
return publicKeyCache.get(appUUID);
} catch (Exception e) {
logger.error("Couldn't find public key", e);
logger.error("Public key retrieval error", e);
throw new HttpClientPublicKeyProviderException(e);
}
}
Expand All @@ -96,18 +96,33 @@ private <T> T get(String url, Map<String, String> headers, ResponseHandler<T> re
}

private class PublicKeyResponseHandler implements ResponseHandler<String> {
private static final String MAX_AGE = "max-age";
private static final String PUBLIC_KEY_STR = "public_key_str";

@Override
public String handleResponse(HttpResponse response) throws IOException {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
if (ttl == null) {
ttl = getMaxAge(response).orElse(configuration.getTimeToLive());
}

HttpEntity entity = response.getEntity();
String responseAsString = EntityUtils.toString(entity, StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
return mapper.readTree(responseAsString).findValue("public_key_str").asText();
return mapper.readTree(responseAsString).findValue(PUBLIC_KEY_STR).asText();
} else {
throw new HttpClientPublicKeyProviderException("Invalid response code returned by server: "
+ response.getStatusLine().getStatusCode());
+ response.getStatusLine().getStatusCode());
}
}

public Optional<Long> getMaxAge(HttpResponse response) {
return Optional.ofNullable(response.getHeaders(HttpHeaders.CACHE_CONTROL))
.flatMap(headers -> Arrays.stream(headers)
.flatMap(header -> Arrays.stream(header.getElements())
.filter(e -> e.getName().equalsIgnoreCase(MAX_AGE))
.map(e -> Long.parseLong(e.getValue())))
.findFirst());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.apache.http.HttpHeaders;

import java.io.IOException;
import java.util.UUID;
Expand Down Expand Up @@ -37,7 +38,8 @@ public static void resetMappings() {

public static void return200() {
WireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo("/mauth/v1/security_tokens/" + EXISTING_CLIENT_APP_UUID.toString() + ".json"))
.willReturn(WireMock.aResponse().withStatus(200).withBody(mockedMauthTokenResponse())));
.willReturn(WireMock.aResponse().withStatus(200).withBody(mockedMauthTokenResponse())
.withHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600, private")));
}

public static void return401() {
Expand Down
1 change: 1 addition & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ object Dependencies extends DependencyUtils {
val sttp: ModuleID = "com.softwaremill.sttp.client3" %% "core" % Version.sttp
val sttpAkkaHttpBackend: ModuleID = "com.softwaremill.sttp.client3" %% "akka-http-backend" % Version.sttp
val scalaLibCompat: ModuleID = "org.scala-lang.modules" %% "scala-collection-compat" % "2.4.3"
val caffeine: ModuleID = "com.github.ben-manes.caffeine" % "caffeine" % "2.9.1"

// TEST DEPENDENCIES
val akkaHttpTestKit: Seq[ModuleID] = Seq(
Expand Down

0 comments on commit a71a7b8

Please sign in to comment.