diff --git a/CHANGELOG.md b/CHANGELOG.md index f121c05b..7fdff6d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/build.sbt b/build.sbt index f2b64517..ccfb896d 100644 --- a/build.sbt +++ b/build.sbt @@ -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) ) diff --git a/modules/mauth-authenticator-apachehttp/src/main/java/com/mdsol/mauth/apache/HttpClientPublicKeyProvider.java b/modules/mauth-authenticator-apachehttp/src/main/java/com/mdsol/mauth/apache/HttpClientPublicKeyProvider.java index be83a746..0b82aeac 100644 --- a/modules/mauth-authenticator-apachehttp/src/main/java/com/mdsol/mauth/apache/HttpClientPublicKeyProvider.java +++ b/modules/mauth-authenticator-apachehttp/src/main/java/com/mdsol/mauth/apache/HttpClientPublicKeyProvider.java @@ -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; @@ -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; @@ -39,28 +37,23 @@ public class HttpClientPublicKeyProvider implements ClientPublicKeyProvider { private final PublicKeyResponseHandler publicKeyResponseHandler; private LoadingCache 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() { - @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 headers = signer.generateRequestHeaders("GET", requestUrlPath, payload, ""); @@ -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); } } @@ -96,18 +96,33 @@ private T get(String url, Map headers, ResponseHandler re } private class PublicKeyResponseHandler implements ResponseHandler { + 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 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()); + } } } diff --git a/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/FakeMAuthServer.java b/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/FakeMAuthServer.java index f1825e6c..77084425 100644 --- a/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/FakeMAuthServer.java +++ b/modules/mauth-test-utils/src/main/java/com/mdsol/mauth/test/utils/FakeMAuthServer.java @@ -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; @@ -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() { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 0920098e..bcf74b76 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -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(